From bed453034fab0a60d346d2a361d69d9ba1278d2d Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:49:11 +0800 Subject: [PATCH] modify devices to use correct executor (sleep, create_task) --- test/registry/example_devices.py | 33 +- unilabos/devices/cnc/grbl_async.py | 11 +- unilabos/devices/cnc/mock.py | 9 +- .../laiyu_liquid/core/laiyu_liquid_main.py | 423 +++++++++--------- .../liquid_handler_abstract.py | 10 +- .../devices/pump_and_valve/runze_async.py | 11 +- .../devices/virtual/virtual_centrifuge.py | 9 +- unilabos/devices/virtual/virtual_column.py | 9 +- unilabos/devices/virtual/virtual_filter.py | 202 ++++----- unilabos/devices/virtual/virtual_heatchill.py | 9 +- unilabos/devices/virtual/virtual_rotavap.py | 273 ++++++----- unilabos/devices/virtual/virtual_separator.py | 11 +- .../devices/virtual/virtual_solenoid_valve.py | 10 +- .../virtual/virtual_solid_dispenser.py | 9 +- unilabos/devices/virtual/virtual_stirrer.py | 11 +- .../devices/virtual/virtual_transferpump.py | 13 +- 16 files changed, 597 insertions(+), 456 deletions(-) diff --git a/test/registry/example_devices.py b/test/registry/example_devices.py index d5b26b2c..d41c7b43 100644 --- a/test/registry/example_devices.py +++ b/test/registry/example_devices.py @@ -3,7 +3,8 @@ """ import asyncio -from typing import Dict, Any, Optional, List +from typing import Dict, Any, List +from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode class SmartPumpController: @@ -14,6 +15,8 @@ class SmartPumpController: 适用于实验室自动化系统中的液体处理任务。 """ + _ros_node: BaseROS2DeviceNode + def __init__(self, device_id: str = "smart_pump_01", port: str = "/dev/ttyUSB0"): """ 初始化智能泵控制器 @@ -30,6 +33,9 @@ class SmartPumpController: self.calibration_factor = 1.0 self.pump_mode = "continuous" # continuous, volume, rate + def post_init(self, ros_node: BaseROS2DeviceNode): + self._ros_node = ros_node + def connect_device(self, timeout: int = 10) -> bool: """ 连接到泵设备 @@ -90,7 +96,7 @@ class SmartPumpController: pump_time = (volume / flow_rate) * 60 # 转换为秒 self.current_flow_rate = flow_rate - await asyncio.sleep(min(pump_time, 3.0)) # 模拟泵送过程 + await self._ros_node.sleep(min(pump_time, 3.0)) # 模拟泵送过程 self.total_volume_pumped += volume self.current_flow_rate = 0.0 @@ -170,6 +176,8 @@ class AdvancedTemperatureController: 适用于需要精确温度控制的化学反应和材料处理过程。 """ + _ros_node: BaseROS2DeviceNode + def __init__(self, controller_id: str = "temp_controller_01"): """ 初始化温度控制器 @@ -185,6 +193,9 @@ class AdvancedTemperatureController: self.pid_enabled = True self.temperature_history: List[Dict] = [] + def post_init(self, ros_node: BaseROS2DeviceNode): + self._ros_node = ros_node + def set_target_temperature(self, temperature: float, rate: float = 10.0) -> bool: """ 设置目标温度 @@ -238,7 +249,7 @@ class AdvancedTemperatureController: } ) - await asyncio.sleep(step_time) + await self._ros_node.sleep(step_time) # 保持历史记录不超过100条 if len(self.temperature_history) > 100: @@ -330,6 +341,8 @@ class MultiChannelAnalyzer: 常用于光谱分析、电化学测量等应用场景。 """ + _ros_node: BaseROS2DeviceNode + def __init__(self, analyzer_id: str = "analyzer_01", channels: int = 8): """ 初始化多通道分析仪 @@ -344,6 +357,9 @@ class MultiChannelAnalyzer: self.is_measuring = False self.sample_rate = 1000 # Hz + def post_init(self, ros_node: BaseROS2DeviceNode): + self._ros_node = ros_node + def configure_channel(self, channel: int, enabled: bool = True, unit: str = "V") -> bool: """ 配置通道 @@ -376,7 +392,7 @@ class MultiChannelAnalyzer: # 模拟数据采集 measurements = [] - for second in range(duration): + for _ in range(duration): timestamp = asyncio.get_event_loop().time() frame_data = {} @@ -391,7 +407,7 @@ class MultiChannelAnalyzer: measurements.append({"timestamp": timestamp, "data": frame_data}) - await asyncio.sleep(1.0) # 每秒采集一次 + await self._ros_node.sleep(1.0) # 每秒采集一次 self.is_measuring = False @@ -465,6 +481,8 @@ class AutomatedDispenser: 集成称重功能,确保分配精度和重现性。 """ + _ros_node: BaseROS2DeviceNode + def __init__(self, dispenser_id: str = "dispenser_01"): """ 初始化自动分配器 @@ -479,6 +497,9 @@ class AutomatedDispenser: self.container_capacity = 1000.0 # mL self.precision_mode = True + def post_init(self, ros_node: BaseROS2DeviceNode): + self._ros_node = ros_node + def move_to_position(self, x: float, y: float, z: float) -> bool: """ 移动到指定位置 @@ -517,7 +538,7 @@ class AutomatedDispenser: if viscosity == "high": dispense_time *= 2 # 高粘度液体需要更长时间 - await asyncio.sleep(min(dispense_time, 5.0)) # 最多等待5秒 + await self._ros_node.sleep(min(dispense_time, 5.0)) # 最多等待5秒 self.dispensed_total += volume diff --git a/unilabos/devices/cnc/grbl_async.py b/unilabos/devices/cnc/grbl_async.py index 7e5ac7f3..3ecd4ba8 100644 --- a/unilabos/devices/cnc/grbl_async.py +++ b/unilabos/devices/cnc/grbl_async.py @@ -12,6 +12,7 @@ from serial import Serial from serial.serialutil import SerialException from unilabos.messages import Point3D +from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode class GrblCNCConnectionError(Exception): @@ -32,6 +33,7 @@ class GrblCNCInfo: class GrblCNCAsync: _status: str = "Offline" _position: Point3D = Point3D(x=0.0, y=0.0, z=0.0) + _ros_node: BaseROS2DeviceNode def __init__(self, port: str, address: str = "1", limits: tuple[int, int, int, int, int, int] = (-150, 150, -200, 0, 0, 60)): self.port = port @@ -58,6 +60,9 @@ class GrblCNCAsync: self._run_future: Optional[Future[Any]] = None self._run_lock = Lock() + def post_init(self, ros_node: BaseROS2DeviceNode): + self._ros_node = ros_node + def _read_all(self): data = self._serial.read_until(b"\n") data_decoded = data.decode() @@ -148,7 +153,7 @@ class GrblCNCAsync: try: await self._query(command) while True: - await asyncio.sleep(0.2) # Wait for 0.5 seconds before polling again + await self._ros_node.sleep(0.2) # Wait for 0.5 seconds before polling again status = await self.get_status() if "Idle" in status: @@ -214,7 +219,7 @@ class GrblCNCAsync: self._pose_number = i self.pose_number_remaining = len(points) - i await self.set_position(point) - await asyncio.sleep(0.5) + await self._ros_node.sleep(0.5) self._step_number = -1 async def stop_operation(self): @@ -235,7 +240,7 @@ class GrblCNCAsync: async def open(self): if self._read_task: raise GrblCNCConnectionError - self._read_task = asyncio.create_task(self._read_loop()) + self._read_task = self._ros_node.create_task(self._read_loop()) try: await self.get_status() diff --git a/unilabos/devices/cnc/mock.py b/unilabos/devices/cnc/mock.py index b8c52f16..ebe96833 100644 --- a/unilabos/devices/cnc/mock.py +++ b/unilabos/devices/cnc/mock.py @@ -2,6 +2,8 @@ import time import asyncio from pydantic import BaseModel +from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode + class Point3D(BaseModel): x: float @@ -14,9 +16,14 @@ def d(a: Point3D, b: Point3D) -> float: class MockCNCAsync: + _ros_node: BaseROS2DeviceNode["MockCNCAsync"] + def __init__(self): self._position: Point3D = Point3D(x=0.0, y=0.0, z=0.0) self._status = "Idle" + + def post_create(self, ros_node): + self._ros_node = ros_node @property def position(self) -> Point3D: @@ -38,5 +45,5 @@ class MockCNCAsync: self._position.x = current_pos.x + (position.x - current_pos.x) / 20 * (i+1) self._position.y = current_pos.y + (position.y - current_pos.y) / 20 * (i+1) self._position.z = current_pos.z + (position.z - current_pos.z) / 20 * (i+1) - await asyncio.sleep(move_time / 20) + await self._ros_node.sleep(move_time / 20) self._status = "Idle" diff --git a/unilabos/devices/laiyu_liquid/core/laiyu_liquid_main.py b/unilabos/devices/laiyu_liquid/core/laiyu_liquid_main.py index 96092556..f369a208 100644 --- a/unilabos/devices/laiyu_liquid/core/laiyu_liquid_main.py +++ b/unilabos/devices/laiyu_liquid/core/laiyu_liquid_main.py @@ -15,108 +15,113 @@ from typing import List, Optional, Dict, Any, Union, Tuple from dataclasses import dataclass from abc import ABC, abstractmethod +from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode + # 基础导入 try: from pylabrobot.resources import Deck, Plate, TipRack, Tip, Resource, Well + PYLABROBOT_AVAILABLE = True except ImportError: # 如果 pylabrobot 不可用,创建基础的模拟类 PYLABROBOT_AVAILABLE = False - + class Resource: def __init__(self, name: str): self.name = name - + class Deck(Resource): pass - + class Plate(Resource): pass - + class TipRack(Resource): pass - + class Tip(Resource): pass - + class Well(Resource): pass + # LaiYu_Liquid 控制器导入 try: - from .controllers.pipette_controller import ( - PipetteController, TipStatus, LiquidClass, LiquidParameters - ) - from .controllers.xyz_controller import ( - XYZController, MachineConfig, CoordinateOrigin, MotorAxis - ) + from .controllers.pipette_controller import PipetteController, TipStatus, LiquidClass, LiquidParameters + from .controllers.xyz_controller import XYZController, MachineConfig, CoordinateOrigin, MotorAxis + CONTROLLERS_AVAILABLE = True except ImportError: CONTROLLERS_AVAILABLE = False + # 创建模拟的控制器类 class PipetteController: def __init__(self, *args, **kwargs): pass - + def connect(self): return True - + def initialize(self): return True - + class XYZController: def __init__(self, *args, **kwargs): pass - + def connect_device(self): return True + logger = logging.getLogger(__name__) class LaiYuLiquidError(RuntimeError): """LaiYu_Liquid 设备异常""" + pass @dataclass class LaiYuLiquidConfig: """LaiYu_Liquid 设备配置""" + port: str = "/dev/cu.usbserial-3130" # RS485转USB端口 address: int = 1 # 设备地址 baudrate: int = 9600 # 波特率 timeout: float = 5.0 # 通信超时时间 - + # 工作台尺寸 deck_width: float = 340.0 # 工作台宽度 (mm) deck_height: float = 250.0 # 工作台高度 (mm) deck_depth: float = 160.0 # 工作台深度 (mm) - + # 移液参数 max_volume: float = 1000.0 # 最大体积 (μL) min_volume: float = 0.1 # 最小体积 (μL) - + # 运动参数 max_speed: float = 100.0 # 最大速度 (mm/s) acceleration: float = 50.0 # 加速度 (mm/s²) - + # 安全参数 safe_height: float = 50.0 # 安全高度 (mm) tip_pickup_depth: float = 10.0 # 吸头拾取深度 (mm) liquid_detection: bool = True # 液面检测 - + # 取枪头相关参数 tip_pickup_speed: int = 30 # 取枪头时的移动速度 (rpm) tip_pickup_acceleration: int = 500 # 取枪头时的加速度 (rpm/s) tip_approach_height: float = 5.0 # 接近枪头时的高度 (mm) tip_pickup_force_depth: float = 2.0 # 强制插入深度 (mm) tip_pickup_retract_height: float = 20.0 # 取枪头后的回退高度 (mm) - + # 丢弃枪头相关参数 tip_drop_height: float = 10.0 # 丢弃枪头时的高度 (mm) tip_drop_speed: int = 50 # 丢弃枪头时的移动速度 (rpm) trash_position: Tuple[float, float, float] = (300.0, 200.0, 0.0) # 垃圾桶位置 (mm) - + # 安全范围配置 deck_width: float = 300.0 # 工作台宽度 (mm) deck_height: float = 200.0 # 工作台高度 (mm) @@ -128,25 +133,25 @@ class LaiYuLiquidConfig: class LaiYuLiquidDeck: """LaiYu_Liquid 工作台管理""" - + def __init__(self, config: LaiYuLiquidConfig): self.config = config self.resources: Dict[str, Resource] = {} self.positions: Dict[str, Tuple[float, float, float]] = {} - + def add_resource(self, name: str, resource: Resource, position: Tuple[float, float, float]): """添加资源到工作台""" self.resources[name] = resource self.positions[name] = position - + def get_resource(self, name: str) -> Optional[Resource]: """获取资源""" return self.resources.get(name) - + def get_position(self, name: str) -> Optional[Tuple[float, float, float]]: """获取资源位置""" return self.positions.get(name) - + def list_resources(self) -> List[str]: """列出所有资源""" return list(self.resources.keys()) @@ -154,8 +159,18 @@ class LaiYuLiquidDeck: class LaiYuLiquidContainer: """LaiYu_Liquid 容器类""" - - def __init__(self, name: str, size_x: float = 0, size_y: float = 0, size_z: float = 0, container_type: str = "", volume: float = 0.0, max_volume: float = 1000.0, lid_height: float = 0.0): + + def __init__( + self, + name: str, + size_x: float = 0, + size_y: float = 0, + size_z: float = 0, + container_type: str = "", + volume: float = 0.0, + max_volume: float = 1000.0, + lid_height: float = 0.0, + ): self.name = name self.size_x = size_x self.size_y = size_y @@ -166,19 +181,19 @@ class LaiYuLiquidContainer: self.max_volume = max_volume self.last_updated = time.time() self.child_resources = {} # 存储子资源 - + @property def is_empty(self) -> bool: return self.volume <= 0.0 - + @property def is_full(self) -> bool: return self.volume >= self.max_volume - + @property def available_volume(self) -> float: return max(0.0, self.max_volume - self.volume) - + def add_volume(self, volume: float) -> bool: """添加体积""" if self.volume + volume <= self.max_volume: @@ -186,7 +201,7 @@ class LaiYuLiquidContainer: self.last_updated = time.time() return True return False - + def remove_volume(self, volume: float) -> bool: """移除体积""" if self.volume >= volume: @@ -194,20 +209,25 @@ class LaiYuLiquidContainer: self.last_updated = time.time() return True return False - + def assign_child_resource(self, resource, location=None): """分配子资源 - 与 PyLabRobot 资源管理系统兼容""" - if hasattr(resource, 'name'): - self.child_resources[resource.name] = { - 'resource': resource, - 'location': location - } + if hasattr(resource, "name"): + self.child_resources[resource.name] = {"resource": resource, "location": location} class LaiYuLiquidTipRack: """LaiYu_Liquid 吸头架类""" - - def __init__(self, name: str, size_x: float = 0, size_y: float = 0, size_z: float = 0, tip_count: int = 96, tip_volume: float = 1000.0): + + def __init__( + self, + name: str, + size_x: float = 0, + size_y: float = 0, + size_z: float = 0, + tip_count: int = 96, + tip_volume: float = 1000.0, + ): self.name = name self.size_x = size_x self.size_y = size_y @@ -216,34 +236,31 @@ class LaiYuLiquidTipRack: self.tip_volume = tip_volume self.tips_available = [True] * tip_count self.child_resources = {} # 存储子资源 - + @property def available_tips(self) -> int: return sum(self.tips_available) - + @property def is_empty(self) -> bool: return self.available_tips == 0 - + def pick_tip(self, position: int) -> bool: """拾取吸头""" if 0 <= position < self.tip_count and self.tips_available[position]: self.tips_available[position] = False return True return False - + def has_tip(self, position: int) -> bool: """检查位置是否有吸头""" if 0 <= position < self.tip_count: return self.tips_available[position] return False - + def assign_child_resource(self, resource, location=None): """分配子资源到指定位置""" - self.child_resources[resource.name] = { - 'resource': resource, - 'location': location - } + self.child_resources[resource.name] = {"resource": resource, "location": location} def get_module_info(): @@ -253,36 +270,32 @@ def get_module_info(): "version": "1.0.0", "description": "LaiYu液体处理工作站模块,提供移液器控制、XYZ轴控制和资源管理功能", "author": "UniLabOS Team", - "capabilities": [ - "移液器控制", - "XYZ轴运动控制", - "吸头架管理", - "板和容器管理", - "资源位置管理" - ], - "dependencies": { - "required": ["serial"], - "optional": ["pylabrobot"] - } + "capabilities": ["移液器控制", "XYZ轴运动控制", "吸头架管理", "板和容器管理", "资源位置管理"], + "dependencies": {"required": ["serial"], "optional": ["pylabrobot"]}, } class LaiYuLiquidBackend: """LaiYu_Liquid 硬件通信后端""" - - def __init__(self, config: LaiYuLiquidConfig, deck: Optional['LaiYuLiquidDeck'] = None): + + _ros_node: BaseROS2DeviceNode + + def __init__(self, config: LaiYuLiquidConfig, deck: Optional["LaiYuLiquidDeck"] = None): self.config = config self.deck = deck # 工作台引用,用于获取资源位置信息 self.pipette_controller = None self.xyz_controller = None self.is_connected = False self.is_initialized = False - + # 状态跟踪 self.current_position = (0.0, 0.0, 0.0) self.tip_attached = False self.current_volume = 0.0 - + + def post_init(self, ros_node: BaseROS2DeviceNode): + self._ros_node = ros_node + def _validate_position(self, x: float, y: float, z: float) -> bool: """验证位置是否在安全范围内""" try: @@ -290,71 +303,71 @@ class LaiYuLiquidBackend: if not (0 <= x <= self.config.deck_width): logger.error(f"X轴位置 {x:.2f}mm 超出范围 [0, {self.config.deck_width}]") return False - + # 检查Y轴范围 if not (0 <= y <= self.config.deck_height): logger.error(f"Y轴位置 {y:.2f}mm 超出范围 [0, {self.config.deck_height}]") return False - + # 检查Z轴范围(负值表示向下,0为工作台表面) if not (-self.config.deck_depth <= z <= self.config.safe_height): logger.error(f"Z轴位置 {z:.2f}mm 超出安全范围 [{-self.config.deck_depth}, {self.config.safe_height}]") return False - + return True except Exception as e: logger.error(f"位置验证失败: {e}") return False - + def _check_hardware_ready(self) -> bool: """检查硬件是否准备就绪""" if not self.is_connected: logger.error("设备未连接") return False - + if CONTROLLERS_AVAILABLE: if self.xyz_controller is None: logger.error("XYZ控制器未初始化") return False - + return True - + async def emergency_stop(self) -> bool: """紧急停止所有运动""" try: logger.warning("执行紧急停止") - + if CONTROLLERS_AVAILABLE and self.xyz_controller: # 停止XYZ控制器 await self.xyz_controller.stop_all_motion() logger.info("XYZ控制器已停止") - + if self.pipette_controller: # 停止移液器控制器 await self.pipette_controller.stop() logger.info("移液器控制器已停止") - + return True except Exception as e: logger.error(f"紧急停止失败: {e}") return False - + async def move_to_safe_position(self) -> bool: """移动到安全位置""" try: if not self._check_hardware_ready(): return False - + safe_position = ( self.config.deck_width / 2, # 工作台中心X self.config.deck_height / 2, # 工作台中心Y - self.config.safe_height # 安全高度Z + self.config.safe_height, # 安全高度Z ) - + if not self._validate_position(*safe_position): logger.error("安全位置无效") return False - + if CONTROLLERS_AVAILABLE and self.xyz_controller: await self.xyz_controller.move_to_work_coord(*safe_position) self.current_position = safe_position @@ -365,33 +378,28 @@ class LaiYuLiquidBackend: self.current_position = safe_position logger.info("模拟移动到安全位置") return True - + except Exception as e: logger.error(f"移动到安全位置失败: {e}") return False - + async def setup(self) -> bool: """设置硬件连接""" try: if CONTROLLERS_AVAILABLE: # 初始化移液器控制器 - self.pipette_controller = PipetteController( - port=self.config.port, - address=self.config.address - ) - + self.pipette_controller = PipetteController(port=self.config.port, address=self.config.address) + # 初始化XYZ控制器 machine_config = MachineConfig() self.xyz_controller = XYZController( - port=self.config.port, - baudrate=self.config.baudrate, - machine_config=machine_config + port=self.config.port, baudrate=self.config.baudrate, machine_config=machine_config ) - + # 连接设备 pipette_connected = await asyncio.to_thread(self.pipette_controller.connect) xyz_connected = await asyncio.to_thread(self.xyz_controller.connect_device) - + if pipette_connected and xyz_connected: self.is_connected = True logger.info("LaiYu_Liquid 硬件连接成功") @@ -404,124 +412,123 @@ class LaiYuLiquidBackend: logger.info("LaiYu_Liquid 运行在模拟模式") self.is_connected = True return True - + except Exception as e: logger.error(f"LaiYu_Liquid 设置失败: {e}") return False - + async def stop(self): """停止设备""" try: - if self.pipette_controller and hasattr(self.pipette_controller, 'disconnect'): + if self.pipette_controller and hasattr(self.pipette_controller, "disconnect"): await asyncio.to_thread(self.pipette_controller.disconnect) - - if self.xyz_controller and hasattr(self.xyz_controller, 'disconnect'): + + if self.xyz_controller and hasattr(self.xyz_controller, "disconnect"): await asyncio.to_thread(self.xyz_controller.disconnect) - + self.is_connected = False self.is_initialized = False logger.info("LaiYu_Liquid 已停止") - + except Exception as e: logger.error(f"LaiYu_Liquid 停止失败: {e}") - + async def move_to(self, x: float, y: float, z: float) -> bool: """移动到指定位置""" try: if not self.is_connected: raise LaiYuLiquidError("设备未连接") - + # 模拟移动 - await asyncio.sleep(0.1) # 模拟移动时间 + await self._ros_node.sleep(0.1) # 模拟移动时间 self.current_position = (x, y, z) logger.debug(f"移动到位置: ({x}, {y}, {z})") return True - + except Exception as e: logger.error(f"移动失败: {e}") return False - + async def pick_up_tip(self, tip_rack: str, position: int) -> bool: """拾取吸头 - 包含真正的Z轴下降控制""" try: # 硬件准备检查 if not self._check_hardware_ready(): return False - + if self.tip_attached: logger.warning("已有吸头附着,无法拾取新吸头") return False - + logger.info(f"开始从 {tip_rack} 位置 {position} 拾取吸头") - + # 获取枪头架位置信息 if self.deck is None: logger.error("工作台未初始化") return False - + tip_position = self.deck.get_position(tip_rack) if tip_position is None: logger.error(f"未找到枪头架 {tip_rack} 的位置信息") return False - + # 计算具体枪头位置(这里简化处理,实际应根据position计算偏移) tip_x, tip_y, tip_z = tip_position - + # 验证所有关键位置的安全性 safe_z = tip_z + self.config.tip_approach_height pickup_z = tip_z - self.config.tip_pickup_force_depth retract_z = tip_z + self.config.tip_pickup_retract_height - - if not (self._validate_position(tip_x, tip_y, safe_z) and - self._validate_position(tip_x, tip_y, pickup_z) and - self._validate_position(tip_x, tip_y, retract_z)): + + if not ( + self._validate_position(tip_x, tip_y, safe_z) + and self._validate_position(tip_x, tip_y, pickup_z) + and self._validate_position(tip_x, tip_y, retract_z) + ): logger.error("枪头拾取位置超出安全范围") return False - + if CONTROLLERS_AVAILABLE and self.xyz_controller: # 真实硬件控制流程 logger.info("使用真实XYZ控制器进行枪头拾取") - + try: # 1. 移动到枪头上方的安全位置 safe_z = tip_z + self.config.tip_approach_height logger.info(f"移动到枪头上方安全位置: ({tip_x:.2f}, {tip_y:.2f}, {safe_z:.2f})") move_success = await asyncio.to_thread( - self.xyz_controller.move_to_work_coord, - tip_x, tip_y, safe_z + self.xyz_controller.move_to_work_coord, tip_x, tip_y, safe_z ) if not move_success: logger.error("移动到枪头上方失败") return False - + # 2. Z轴下降到枪头位置 pickup_z = tip_z - self.config.tip_pickup_force_depth logger.info(f"Z轴下降到枪头拾取位置: {pickup_z:.2f}mm") z_down_success = await asyncio.to_thread( - self.xyz_controller.move_to_work_coord, - tip_x, tip_y, pickup_z + self.xyz_controller.move_to_work_coord, tip_x, tip_y, pickup_z ) if not z_down_success: logger.error("Z轴下降到枪头位置失败") return False - + # 3. 等待一小段时间确保枪头牢固附着 - await asyncio.sleep(0.2) - + await self._ros_node.sleep(0.2) + # 4. Z轴上升到回退高度 retract_z = tip_z + self.config.tip_pickup_retract_height logger.info(f"Z轴上升到回退高度: {retract_z:.2f}mm") z_up_success = await asyncio.to_thread( - self.xyz_controller.move_to_work_coord, - tip_x, tip_y, retract_z + self.xyz_controller.move_to_work_coord, tip_x, tip_y, retract_z ) if not z_up_success: logger.error("Z轴上升失败") return False - + # 5. 更新当前位置 self.current_position = (tip_x, tip_y, retract_z) - + except Exception as move_error: logger.error(f"枪头拾取过程中发生错误: {move_error}") # 尝试移动到安全位置 @@ -529,35 +536,35 @@ class LaiYuLiquidBackend: await self.emergency_stop() await self.move_to_safe_position() return False - + else: # 模拟模式 logger.info("模拟模式:执行枪头拾取动作") - await asyncio.sleep(1.0) # 模拟整个拾取过程的时间 + await self._ros_node.sleep(1.0) # 模拟整个拾取过程的时间 self.current_position = (tip_x, tip_y, tip_z + self.config.tip_pickup_retract_height) - + # 6. 标记枪头已附着 self.tip_attached = True logger.info("吸头拾取成功") return True - + except Exception as e: logger.error(f"拾取吸头失败: {e}") return False - + async def drop_tip(self, location: str = "trash") -> bool: """丢弃吸头 - 包含真正的Z轴控制""" try: # 硬件准备检查 if not self._check_hardware_ready(): return False - + if not self.tip_attached: logger.warning("没有吸头附着,无需丢弃") return True - + logger.info(f"开始丢弃吸头到 {location}") - + # 确定丢弃位置 if location == "trash": # 使用配置中的垃圾桶位置 @@ -567,48 +574,48 @@ class LaiYuLiquidBackend: if self.deck is None: logger.error("工作台未初始化") return False - + drop_position = self.deck.get_position(location) if drop_position is None: logger.error(f"未找到丢弃位置 {location} 的信息") return False drop_x, drop_y, drop_z = drop_position - + # 验证丢弃位置的安全性 safe_z = drop_z + self.config.safe_height drop_height_z = drop_z + self.config.tip_drop_height - - if not (self._validate_position(drop_x, drop_y, safe_z) and - self._validate_position(drop_x, drop_y, drop_height_z)): + + if not ( + self._validate_position(drop_x, drop_y, safe_z) + and self._validate_position(drop_x, drop_y, drop_height_z) + ): logger.error("枪头丢弃位置超出安全范围") return False - + if CONTROLLERS_AVAILABLE and self.xyz_controller: # 真实硬件控制流程 logger.info("使用真实XYZ控制器进行枪头丢弃") - + try: # 1. 移动到丢弃位置上方的安全高度 safe_z = drop_z + self.config.tip_drop_height logger.info(f"移动到丢弃位置上方: ({drop_x:.2f}, {drop_y:.2f}, {safe_z:.2f})") move_success = await asyncio.to_thread( - self.xyz_controller.move_to_work_coord, - drop_x, drop_y, safe_z + self.xyz_controller.move_to_work_coord, drop_x, drop_y, safe_z ) if not move_success: logger.error("移动到丢弃位置上方失败") return False - + # 2. Z轴下降到丢弃高度 logger.info(f"Z轴下降到丢弃高度: {drop_z:.2f}mm") z_down_success = await asyncio.to_thread( - self.xyz_controller.move_to_work_coord, - drop_x, drop_y, drop_z + self.xyz_controller.move_to_work_coord, drop_x, drop_y, drop_z ) if not z_down_success: logger.error("Z轴下降到丢弃位置失败") return False - + # 3. 执行枪头弹出动作(如果有移液器控制器) if self.pipette_controller: try: @@ -617,23 +624,22 @@ class LaiYuLiquidBackend: logger.info("执行枪头弹出命令") except Exception as e: logger.warning(f"枪头弹出命令失败: {e}") - + # 4. 等待一小段时间确保枪头完全脱离 - await asyncio.sleep(0.3) - + await self._ros_node.sleep(0.3) + # 5. Z轴上升到安全高度 logger.info(f"Z轴上升到安全高度: {safe_z:.2f}mm") z_up_success = await asyncio.to_thread( - self.xyz_controller.move_to_work_coord, - drop_x, drop_y, safe_z + self.xyz_controller.move_to_work_coord, drop_x, drop_y, safe_z ) if not z_up_success: logger.error("Z轴上升失败") return False - + # 6. 更新当前位置 self.current_position = (drop_x, drop_y, safe_z) - + except Exception as drop_error: logger.error(f"枪头丢弃过程中发生错误: {drop_error}") # 尝试移动到安全位置 @@ -641,63 +647,63 @@ class LaiYuLiquidBackend: await self.emergency_stop() await self.move_to_safe_position() return False - + else: # 模拟模式 logger.info("模拟模式:执行枪头丢弃动作") - await asyncio.sleep(0.8) # 模拟整个丢弃过程的时间 + await self._ros_node.sleep(0.8) # 模拟整个丢弃过程的时间 self.current_position = (drop_x, drop_y, drop_z + self.config.tip_drop_height) - + # 7. 标记枪头已脱离,清空体积 self.tip_attached = False self.current_volume = 0.0 logger.info("吸头丢弃成功") return True - + except Exception as e: logger.error(f"丢弃吸头失败: {e}") return False - + async def aspirate(self, volume: float, location: str) -> bool: """吸取液体""" try: if not self.is_connected: raise LaiYuLiquidError("设备未连接") - + if not self.tip_attached: raise LaiYuLiquidError("没有吸头附着") - + if volume <= 0 or volume > self.config.max_volume: raise LaiYuLiquidError(f"体积超出范围: {volume}") - + # 模拟吸取 - await asyncio.sleep(0.3) + await self._ros_node.sleep(0.3) self.current_volume += volume logger.debug(f"从 {location} 吸取 {volume} μL") return True - + except Exception as e: logger.error(f"吸取失败: {e}") return False - + async def dispense(self, volume: float, location: str) -> bool: """分配液体""" try: if not self.is_connected: raise LaiYuLiquidError("设备未连接") - + if not self.tip_attached: raise LaiYuLiquidError("没有吸头附着") - + if volume <= 0 or volume > self.current_volume: raise LaiYuLiquidError(f"分配体积无效: {volume}") - + # 模拟分配 - await asyncio.sleep(0.3) + await self._ros_node.sleep(0.3) self.current_volume -= volume logger.debug(f"向 {location} 分配 {volume} μL") return True - + except Exception as e: logger.error(f"分配失败: {e}") return False @@ -705,7 +711,7 @@ class LaiYuLiquidBackend: class LaiYuLiquid: """LaiYu_Liquid 主要接口类""" - + def __init__(self, config: Optional[LaiYuLiquidConfig] = None, **kwargs): # 如果传入了关键字参数,创建配置对象 if kwargs and config is None: @@ -717,37 +723,37 @@ class LaiYuLiquid: self.config = LaiYuLiquidConfig(**config_params) else: self.config = config or LaiYuLiquidConfig() - + # 先创建deck,然后传递给backend self.deck = LaiYuLiquidDeck(self.config) self.backend = LaiYuLiquidBackend(self.config, self.deck) self.is_setup = False - + @property def current_position(self) -> Tuple[float, float, float]: """获取当前位置""" return self.backend.current_position - + @property def current_volume(self) -> float: """获取当前体积""" return self.backend.current_volume - + @property def is_connected(self) -> bool: """获取连接状态""" return self.backend.is_connected - + @property def is_initialized(self) -> bool: """获取初始化状态""" return self.backend.is_initialized - + @property def tip_attached(self) -> bool: """获取吸头附着状态""" return self.backend.tip_attached - + async def setup(self) -> bool: """设置液体处理器""" try: @@ -759,27 +765,28 @@ class LaiYuLiquid: except Exception as e: logger.error(f"LaiYu_Liquid 设置失败: {e}") return False - + async def stop(self): """停止液体处理器""" await self.backend.stop() self.is_setup = False - - async def transfer(self, source: str, target: str, volume: float, - tip_rack: str = "tip_rack_1", tip_position: int = 0) -> bool: + + async def transfer( + self, source: str, target: str, volume: float, tip_rack: str = "tip_rack_1", tip_position: int = 0 + ) -> bool: """液体转移""" try: if not self.is_setup: raise LaiYuLiquidError("设备未设置") - + # 获取源和目标位置 source_pos = self.deck.get_position(source) target_pos = self.deck.get_position(target) tip_pos = self.deck.get_position(tip_rack) - + if not all([source_pos, target_pos, tip_pos]): raise LaiYuLiquidError("位置信息不完整") - + # 执行转移步骤 steps = [ ("移动到吸头架", self.backend.move_to(*tip_pos)), @@ -788,22 +795,22 @@ class LaiYuLiquid: ("吸取液体", self.backend.aspirate(volume, source)), ("移动到目标位置", self.backend.move_to(*target_pos)), ("分配液体", self.backend.dispense(volume, target)), - ("丢弃吸头", self.backend.drop_tip()) + ("丢弃吸头", self.backend.drop_tip()), ] - + for step_name, step_coro in steps: logger.debug(f"执行步骤: {step_name}") success = await step_coro if not success: raise LaiYuLiquidError(f"步骤失败: {step_name}") - + logger.info(f"液体转移完成: {source} -> {target}, {volume} μL") return True - + except Exception as e: logger.error(f"液体转移失败: {e}") return False - + def add_resource(self, name: str, resource_type: str, position: Tuple[float, float, float]): """添加资源到工作台""" if resource_type == "plate": @@ -812,9 +819,9 @@ class LaiYuLiquid: resource = TipRack(name) else: resource = Resource(name) - + self.deck.add_resource(name, resource, position) - + def get_status(self) -> Dict[str, Any]: """获取设备状态""" return { @@ -823,59 +830,59 @@ class LaiYuLiquid: "current_position": self.backend.current_position, "tip_attached": self.backend.tip_attached, "current_volume": self.backend.current_volume, - "resources": self.deck.list_resources() + "resources": self.deck.list_resources(), } def create_quick_setup() -> LaiYuLiquidDeck: """ 创建快速设置的LaiYu液体处理工作站 - + Returns: LaiYuLiquidDeck: 配置好的工作台实例 """ # 创建默认配置 config = LaiYuLiquidConfig() - + # 创建工作台 deck = LaiYuLiquidDeck(config) - + # 导入资源创建函数 try: from .laiyu_liquid_res import ( create_tip_rack_1000ul, create_tip_rack_200ul, create_96_well_plate, - create_waste_container + create_waste_container, ) - + # 添加基本资源 tip_rack_1000 = create_tip_rack_1000ul("tip_rack_1000") tip_rack_200 = create_tip_rack_200ul("tip_rack_200") plate_96 = create_96_well_plate("plate_96") waste = create_waste_container("waste") - + # 添加到工作台 deck.add_resource("tip_rack_1000", tip_rack_1000, (50, 50, 0)) deck.add_resource("tip_rack_200", tip_rack_200, (150, 50, 0)) deck.add_resource("plate_96", plate_96, (250, 50, 0)) deck.add_resource("waste", waste, (50, 150, 0)) - + except ImportError: # 如果资源模块不可用,创建空的工作台 logger.warning("资源模块不可用,创建空的工作台") - + return deck __all__ = [ "LaiYuLiquid", - "LaiYuLiquidBackend", + "LaiYuLiquidBackend", "LaiYuLiquidConfig", "LaiYuLiquidDeck", "LaiYuLiquidContainer", "LaiYuLiquidTipRack", "LaiYuLiquidError", "create_quick_setup", - "get_module_info" -] \ No newline at end of file + "get_module_info", +] diff --git a/unilabos/devices/liquid_handling/liquid_handler_abstract.py b/unilabos/devices/liquid_handling/liquid_handler_abstract.py index 69b757b5..32e370fe 100644 --- a/unilabos/devices/liquid_handling/liquid_handler_abstract.py +++ b/unilabos/devices/liquid_handling/liquid_handler_abstract.py @@ -25,6 +25,8 @@ from pylabrobot.resources import ( Tip, ) +from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode + class LiquidHandlerMiddleware(LiquidHandler): def __init__(self, backend: LiquidHandlerBackend, deck: Deck, simulator: bool = False, channel_num: int = 8): @@ -536,6 +538,7 @@ class LiquidHandlerMiddleware(LiquidHandler): class LiquidHandlerAbstract(LiquidHandlerMiddleware): """Extended LiquidHandler with additional operations.""" support_touch_tip = True + _ros_node: BaseROS2DeviceNode def __init__(self, backend: LiquidHandlerBackend, deck: Deck, simulator: bool=False, channel_num:int = 8): """Initialize a LiquidHandler. @@ -548,8 +551,11 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware): self.group_info = dict() super().__init__(backend, deck, simulator, channel_num) + def post_init(self, ros_node: BaseROS2DeviceNode): + self._ros_node = ros_node + @classmethod - def set_liquid(self, wells: list[Well], liquid_names: list[str], volumes: list[float]): + def set_liquid(cls, wells: list[Well], liquid_names: list[str], volumes: list[float]): """Set the liquid in a well.""" for well, liquid_name, volume in zip(wells, liquid_names, volumes): well.set_liquids([(liquid_name, volume)]) # type: ignore @@ -1081,7 +1087,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware): print(f"Waiting time: {msg}") print(f"Current time: {time.strftime('%H:%M:%S')}") print(f"Time to finish: {time.strftime('%H:%M:%S', time.localtime(time.time() + seconds))}") - await asyncio.sleep(seconds) + await self._ros_node.sleep(seconds) if msg: print(f"Done: {msg}") print(f"Current time: {time.strftime('%H:%M:%S')}") diff --git a/unilabos/devices/pump_and_valve/runze_async.py b/unilabos/devices/pump_and_valve/runze_async.py index 9b8d649e..7bc11155 100644 --- a/unilabos/devices/pump_and_valve/runze_async.py +++ b/unilabos/devices/pump_and_valve/runze_async.py @@ -8,6 +8,8 @@ import serial.tools.list_ports from serial import Serial from serial.serialutil import SerialException +from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode + class RunzeSyringePumpMode(Enum): Normal = 0 @@ -77,6 +79,8 @@ class RunzeSyringePumpInfo: class RunzeSyringePumpAsync: + _ros_node: BaseROS2DeviceNode + def __init__(self, port: str, address: str = "1", volume: float = 25000, mode: RunzeSyringePumpMode = None): self.port = port self.address = address @@ -102,6 +106,9 @@ class RunzeSyringePumpAsync: self._run_future: Optional[Future[Any]] = None self._run_lock = Lock() + def post_init(self, ros_node: BaseROS2DeviceNode): + self._ros_node = ros_node + def _adjust_total_steps(self): self.total_steps = 6000 if self.mode == RunzeSyringePumpMode.Normal else 48000 self.total_steps_vel = 48000 if self.mode == RunzeSyringePumpMode.AccuratePosVel else 6000 @@ -182,7 +189,7 @@ class RunzeSyringePumpAsync: try: await self._query(command) while True: - await asyncio.sleep(0.5) # Wait for 0.5 seconds before polling again + await self._ros_node.sleep(0.5) # Wait for 0.5 seconds before polling again status = await self.query_device_status() if status == '`': @@ -364,7 +371,7 @@ class RunzeSyringePumpAsync: if self._read_task: raise RunzeSyringePumpConnectionError - self._read_task = asyncio.create_task(self._read_loop()) + self._read_task = self._ros_node.create_task(self._read_loop()) try: await self.query_device_status() diff --git a/unilabos/devices/virtual/virtual_centrifuge.py b/unilabos/devices/virtual/virtual_centrifuge.py index 79f9dce0..afce45a9 100644 --- a/unilabos/devices/virtual/virtual_centrifuge.py +++ b/unilabos/devices/virtual/virtual_centrifuge.py @@ -3,9 +3,13 @@ import logging import time as time_module from typing import Dict, Any, Optional +from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode + class VirtualCentrifuge: """Virtual centrifuge device - 简化版,只保留核心功能""" + + _ros_node: BaseROS2DeviceNode def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs): # 处理可能的不同调用方式 @@ -32,6 +36,9 @@ class VirtualCentrifuge: for key, value in kwargs.items(): if key not in skip_keys and not hasattr(self, key): setattr(self, key, value) + + def post_init(self, ros_node: BaseROS2DeviceNode): + self._ros_node = ros_node async def initialize(self) -> bool: """Initialize virtual centrifuge""" @@ -132,7 +139,7 @@ class VirtualCentrifuge: break # 每秒更新一次 - await asyncio.sleep(1.0) + await self._ros_node.sleep(1.0) # 离心完成 self.data.update({ diff --git a/unilabos/devices/virtual/virtual_column.py b/unilabos/devices/virtual/virtual_column.py index 892a320f..539f302a 100644 --- a/unilabos/devices/virtual/virtual_column.py +++ b/unilabos/devices/virtual/virtual_column.py @@ -2,9 +2,13 @@ import asyncio import logging from typing import Dict, Any, Optional +from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode + class VirtualColumn: """Virtual column device for RunColumn protocol 🏛️""" + _ros_node: BaseROS2DeviceNode + def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs): # 处理可能的不同调用方式 if device_id is None and 'id' in kwargs: @@ -28,6 +32,9 @@ class VirtualColumn: print(f"🏛️ === 虚拟色谱柱 {self.device_id} 已创建 === ✨") print(f"📏 柱参数: 流速={self._max_flow_rate}mL/min | 长度={self._column_length}cm | 直径={self._column_diameter}cm 🔬") + def post_init(self, ros_node: BaseROS2DeviceNode): + self._ros_node = ros_node + async def initialize(self) -> bool: """Initialize virtual column 🚀""" self.logger.info(f"🔧 初始化虚拟色谱柱 {self.device_id} ✨") @@ -101,7 +108,7 @@ class VirtualColumn: step_time = separation_time / steps for i in range(steps): - await asyncio.sleep(step_time) + await self._ros_node.sleep(step_time) progress = (i + 1) / steps * 100 volume_processed = (i + 1) * 5.0 # 假设每步处理5mL diff --git a/unilabos/devices/virtual/virtual_filter.py b/unilabos/devices/virtual/virtual_filter.py index ffd8f549..98effc99 100644 --- a/unilabos/devices/virtual/virtual_filter.py +++ b/unilabos/devices/virtual/virtual_filter.py @@ -4,70 +4,76 @@ import time as time_module from typing import Dict, Any, Optional from unilabos.compile.utils.vessel_parser import get_vessel +from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode class VirtualFilter: """Virtual filter device - 完全按照 Filter.action 规范 🌊""" - + + _ros_node: BaseROS2DeviceNode + def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs): - if device_id is None and 'id' in kwargs: - device_id = kwargs.pop('id') - if config is None and 'config' in kwargs: - config = kwargs.pop('config') - + if device_id is None and "id" in kwargs: + device_id = kwargs.pop("id") + if config is None and "config" in kwargs: + config = kwargs.pop("config") + self.device_id = device_id or "unknown_filter" self.config = config or {} self.logger = logging.getLogger(f"VirtualFilter.{self.device_id}") self.data = {} - + # 从config或kwargs中获取配置参数 - self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL') - self._max_temp = self.config.get('max_temp') or kwargs.get('max_temp', 100.0) - self._max_stir_speed = self.config.get('max_stir_speed') or kwargs.get('max_stir_speed', 1000.0) - self._max_volume = self.config.get('max_volume') or kwargs.get('max_volume', 500.0) - + self.port = self.config.get("port") or kwargs.get("port", "VIRTUAL") + self._max_temp = self.config.get("max_temp") or kwargs.get("max_temp", 100.0) + self._max_stir_speed = self.config.get("max_stir_speed") or kwargs.get("max_stir_speed", 1000.0) + self._max_volume = self.config.get("max_volume") or kwargs.get("max_volume", 500.0) + # 处理其他kwargs参数 - skip_keys = {'port', 'max_temp', 'max_stir_speed', 'max_volume'} + skip_keys = {"port", "max_temp", "max_stir_speed", "max_volume"} for key, value in kwargs.items(): if key not in skip_keys and not hasattr(self, key): setattr(self, key, value) - + + def post_init(self, ros_node: BaseROS2DeviceNode): + self._ros_node = ros_node + async def initialize(self) -> bool: """Initialize virtual filter 🚀""" self.logger.info(f"🔧 初始化虚拟过滤器 {self.device_id} ✨") - + # 按照 Filter.action 的 feedback 字段初始化 - self.data.update({ - "status": "Idle", - "progress": 0.0, # Filter.action feedback - "current_temp": 25.0, # Filter.action feedback - "filtered_volume": 0.0, # Filter.action feedback - "message": "Ready for filtration" - }) - + self.data.update( + { + "status": "Idle", + "progress": 0.0, # Filter.action feedback + "current_temp": 25.0, # Filter.action feedback + "filtered_volume": 0.0, # Filter.action feedback + "message": "Ready for filtration", + } + ) + self.logger.info(f"✅ 过滤器 {self.device_id} 初始化完成 🌊") return True - + async def cleanup(self) -> bool: """Cleanup virtual filter 🧹""" self.logger.info(f"🧹 清理虚拟过滤器 {self.device_id} 🔚") - - self.data.update({ - "status": "Offline" - }) - + + self.data.update({"status": "Offline"}) + self.logger.info(f"✅ 过滤器 {self.device_id} 清理完成 💤") return True - + async def filter( - self, + self, vessel: dict, filtrate_vessel: dict = {}, - stir: bool = False, - stir_speed: float = 300.0, - temp: float = 25.0, - continue_heatchill: bool = False, - volume: float = 0.0 + stir: bool = False, + stir_speed: float = 300.0, + temp: float = 25.0, + continue_heatchill: bool = False, + volume: float = 0.0, ) -> bool: """Execute filter action - 完全按照 Filter.action 参数 🌊""" vessel_id, _ = get_vessel(vessel) @@ -79,59 +85,52 @@ class VirtualFilter: temp = 25.0 # 0度自动设置为室温 self.logger.info(f"🌡️ 温度自动调整: {original_temp}°C → {temp}°C (室温) 🏠") elif temp < 4.0: - temp = 4.0 # 小于4度自动设置为4度 + temp = 4.0 # 小于4度自动设置为4度 self.logger.info(f"🌡️ 温度自动调整: {original_temp}°C → {temp}°C (最低温度) ❄️") - + self.logger.info(f"🌊 开始过滤操作: {vessel_id} → {filtrate_vessel_id} 🚰") self.logger.info(f" 🌪️ 搅拌: {stir} ({stir_speed} RPM)") self.logger.info(f" 🌡️ 温度: {temp}°C") self.logger.info(f" 💧 体积: {volume}mL") self.logger.info(f" 🔥 保持加热: {continue_heatchill}") - + # 验证参数 if temp > self._max_temp or temp < 4.0: error_msg = f"🌡️ 温度 {temp}°C 超出范围 (4-{self._max_temp}°C) ⚠️" self.logger.error(f"❌ {error_msg}") - self.data.update({ - "status": f"Error: 温度超出范围 ⚠️", - "message": error_msg - }) + self.data.update({"status": f"Error: 温度超出范围 ⚠️", "message": error_msg}) return False - + if stir and stir_speed > self._max_stir_speed: error_msg = f"🌪️ 搅拌速度 {stir_speed} RPM 超出范围 (0-{self._max_stir_speed} RPM) ⚠️" self.logger.error(f"❌ {error_msg}") - self.data.update({ - "status": f"Error: 搅拌速度超出范围 ⚠️", - "message": error_msg - }) + self.data.update({"status": f"Error: 搅拌速度超出范围 ⚠️", "message": error_msg}) return False - + if volume > self._max_volume: error_msg = f"💧 过滤体积 {volume} mL 超出范围 (0-{self._max_volume} mL) ⚠️" self.logger.error(f"❌ {error_msg}") - self.data.update({ - "status": f"Error", - "message": error_msg - }) + self.data.update({"status": f"Error", "message": error_msg}) return False - + # 开始过滤 filter_volume = volume if volume > 0 else 50.0 self.logger.info(f"🚀 开始过滤 {filter_volume}mL 液体 💧") - - self.data.update({ - "status": f"Running", - "current_temp": temp, - "filtered_volume": 0.0, - "progress": 0.0, - "message": f"🚀 Starting filtration: {vessel_id} → {filtrate_vessel_id}" - }) - + + self.data.update( + { + "status": f"Running", + "current_temp": temp, + "filtered_volume": 0.0, + "progress": 0.0, + "message": f"🚀 Starting filtration: {vessel_id} → {filtrate_vessel_id}", + } + ) + try: # 过滤过程 - 实时更新进度 start_time = time_module.time() - + # 根据体积和搅拌估算过滤时间 base_time = filter_volume / 5.0 # 5mL/s 基础速度 if stir: @@ -140,78 +139,79 @@ class VirtualFilter: if temp > 50.0: base_time *= 0.7 # 高温加速过滤 self.logger.info(f"🔥 高温加速过滤,预计时间减少30% ⚡") - + filter_time = max(base_time, 10.0) # 最少10秒 self.logger.info(f"⏱️ 预计过滤时间: {filter_time:.1f}秒 ⌛") - + while True: current_time = time_module.time() elapsed = current_time - start_time remaining = max(0, filter_time - elapsed) progress = min(100.0, (elapsed / filter_time) * 100) current_filtered = (progress / 100.0) * filter_volume - + # 更新状态 - 按照 Filter.action feedback 字段 status_msg = f"🌊 过滤中: {vessel}" if stir: status_msg += f" | 🌪️ 搅拌: {stir_speed} RPM" status_msg += f" | 🌡️ {temp}°C | 📊 {progress:.1f}% | 💧 已过滤: {current_filtered:.1f}mL" - - self.data.update({ - "progress": progress, # Filter.action feedback - "current_temp": temp, # Filter.action feedback - "filtered_volume": current_filtered, # Filter.action feedback - "status": "Running", - "message": f"🌊 Filtering: {progress:.1f}% complete, {current_filtered:.1f}mL filtered" - }) - + + self.data.update( + { + "progress": progress, # Filter.action feedback + "current_temp": temp, # Filter.action feedback + "filtered_volume": current_filtered, # Filter.action feedback + "status": "Running", + "message": f"🌊 Filtering: {progress:.1f}% complete, {current_filtered:.1f}mL filtered", + } + ) + # 进度日志(每25%打印一次) if progress >= 25 and progress % 25 < 1: self.logger.info(f"📊 过滤进度: {progress:.0f}% | 💧 {current_filtered:.1f}mL 完成 ✨") - + if remaining <= 0: break - - await asyncio.sleep(1.0) - + + await self._ros_node.sleep(1.0) + # 过滤完成 final_temp = temp if continue_heatchill else 25.0 final_status = f"✅ 过滤完成: {vessel} | 💧 {filter_volume}mL → {filtrate_vessel}" if continue_heatchill: final_status += " | 🔥 继续加热搅拌" self.logger.info(f"🔥 继续保持加热搅拌状态 🌪️") - - self.data.update({ - "status": final_status, - "progress": 100.0, # Filter.action feedback - "current_temp": final_temp, # Filter.action feedback - "filtered_volume": filter_volume, # Filter.action feedback - "message": f"✅ Filtration completed: {filter_volume}mL filtered from {vessel_id}" - }) - + + self.data.update( + { + "status": final_status, + "progress": 100.0, # Filter.action feedback + "current_temp": final_temp, # Filter.action feedback + "filtered_volume": filter_volume, # Filter.action feedback + "message": f"✅ Filtration completed: {filter_volume}mL filtered from {vessel_id}", + } + ) + self.logger.info(f"🎉 过滤完成! 💧 {filter_volume}mL 从 {vessel_id} 过滤到 {filtrate_vessel_id} ✨") self.logger.info(f"📊 最终状态: 温度 {final_temp}°C | 进度 100% | 体积 {filter_volume}mL 🏁") return True - + except Exception as e: error_msg = f"过滤过程中发生错误: {str(e)} 💥" self.logger.error(f"❌ {error_msg}") - self.data.update({ - "status": f"Error", - "message": f"❌ Filtration failed: {str(e)}" - }) + self.data.update({"status": f"Error", "message": f"❌ Filtration failed: {str(e)}"}) return False - + # === 核心状态属性 - 按照 Filter.action feedback 字段 === @property def status(self) -> str: return self.data.get("status", "❓ Unknown") - + @property def progress(self) -> float: """Filter.action feedback 字段 📊""" return self.data.get("progress", 0.0) - + @property def current_temp(self) -> float: """Filter.action feedback 字段 🌡️""" @@ -230,15 +230,15 @@ class VirtualFilter: @property def message(self) -> str: return self.data.get("message", "") - + @property def max_temp(self) -> float: return self._max_temp - + @property def max_stir_speed(self) -> float: return self._max_stir_speed - + @property def max_volume(self) -> float: - return self._max_volume \ No newline at end of file + return self._max_volume diff --git a/unilabos/devices/virtual/virtual_heatchill.py b/unilabos/devices/virtual/virtual_heatchill.py index 2f7e555b..29a9fd28 100644 --- a/unilabos/devices/virtual/virtual_heatchill.py +++ b/unilabos/devices/virtual/virtual_heatchill.py @@ -3,9 +3,13 @@ import logging import time as time_module # 重命名time模块,避免与参数冲突 from typing import Dict, Any +from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode + class VirtualHeatChill: """Virtual heat chill device for HeatChillProtocol testing 🌡️""" + _ros_node: BaseROS2DeviceNode + def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs): # 处理可能的不同调用方式 if device_id is None and 'id' in kwargs: @@ -35,6 +39,9 @@ class VirtualHeatChill: print(f"🌡️ === 虚拟温控设备 {self.device_id} 已创建 === ✨") print(f"🔥 温度范围: {self._min_temp}°C ~ {self._max_temp}°C | 🌪️ 最大搅拌: {self._max_stir_speed} RPM") + def post_init(self, ros_node: BaseROS2DeviceNode): + self._ros_node = ros_node + async def initialize(self) -> bool: """Initialize virtual heat chill 🚀""" self.logger.info(f"🔧 初始化虚拟温控设备 {self.device_id} ✨") @@ -177,7 +184,7 @@ class VirtualHeatChill: break # 等待1秒后再次检查 - await asyncio.sleep(1.0) + await self._ros_node.sleep(1.0) # 操作完成 final_stir_info = f" | 🌪️ 搅拌: {stir_speed} RPM" if stir else "" diff --git a/unilabos/devices/virtual/virtual_rotavap.py b/unilabos/devices/virtual/virtual_rotavap.py index 23e24b7e..5e85d35c 100644 --- a/unilabos/devices/virtual/virtual_rotavap.py +++ b/unilabos/devices/virtual/virtual_rotavap.py @@ -3,13 +3,19 @@ import logging import time as time_module from typing import Dict, Any, Optional +from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode + + def debug_print(message): """调试输出 🔍""" print(f"🌪️ [ROTAVAP] {message}", flush=True) + class VirtualRotavap: """Virtual rotary evaporator device - 简化版,只保留核心功能 🌪️""" + _ros_node: BaseROS2DeviceNode + def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs): # 处理可能的不同调用方式 if device_id is None and "id" in kwargs: @@ -38,56 +44,65 @@ class VirtualRotavap: print(f"🌪️ === 虚拟旋转蒸发仪 {self.device_id} 已创建 === ✨") print(f"🔥 温度范围: 10°C ~ {self._max_temp}°C | 🌀 转速范围: 10 ~ {self._max_rotation_speed} RPM") + def post_init(self, ros_node: BaseROS2DeviceNode): + self._ros_node = ros_node + async def initialize(self) -> bool: """Initialize virtual rotary evaporator 🚀""" self.logger.info(f"🔧 初始化虚拟旋转蒸发仪 {self.device_id} ✨") - + # 只保留核心状态 - self.data.update({ - "status": "🏠 待机中", - "rotavap_state": "Ready", # Ready, Evaporating, Completed, Error - "current_temp": 25.0, - "target_temp": 25.0, - "rotation_speed": 0.0, - "vacuum_pressure": 1.0, # 大气压 - "evaporated_volume": 0.0, - "progress": 0.0, - "remaining_time": 0.0, - "message": "🌪️ Ready for evaporation" - }) - + self.data.update( + { + "status": "🏠 待机中", + "rotavap_state": "Ready", # Ready, Evaporating, Completed, Error + "current_temp": 25.0, + "target_temp": 25.0, + "rotation_speed": 0.0, + "vacuum_pressure": 1.0, # 大气压 + "evaporated_volume": 0.0, + "progress": 0.0, + "remaining_time": 0.0, + "message": "🌪️ Ready for evaporation", + } + ) + self.logger.info(f"✅ 旋转蒸发仪 {self.device_id} 初始化完成 🌪️") - self.logger.info(f"📊 设备规格: 温度范围 10°C ~ {self._max_temp}°C | 转速范围 10 ~ {self._max_rotation_speed} RPM") + self.logger.info( + f"📊 设备规格: 温度范围 10°C ~ {self._max_temp}°C | 转速范围 10 ~ {self._max_rotation_speed} RPM" + ) return True async def cleanup(self) -> bool: """Cleanup virtual rotary evaporator 🧹""" self.logger.info(f"🧹 清理虚拟旋转蒸发仪 {self.device_id} 🔚") - - self.data.update({ - "status": "💤 离线", - "rotavap_state": "Offline", - "current_temp": 25.0, - "rotation_speed": 0.0, - "vacuum_pressure": 1.0, - "message": "💤 System offline" - }) - + + self.data.update( + { + "status": "💤 离线", + "rotavap_state": "Offline", + "current_temp": 25.0, + "rotation_speed": 0.0, + "vacuum_pressure": 1.0, + "message": "💤 System offline", + } + ) + self.logger.info(f"✅ 旋转蒸发仪 {self.device_id} 清理完成 💤") return True async def evaporate( - self, - vessel: str, - pressure: float = 0.1, - temp: float = 60.0, + self, + vessel: str, + pressure: float = 0.1, + temp: float = 60.0, time: float = 180.0, stir_speed: float = 100.0, solvent: str = "", - **kwargs + **kwargs, ) -> bool: """Execute evaporate action - 简化版 🌪️""" - + # 🔧 新增:确保time参数是数值类型 if isinstance(time, str): try: @@ -98,31 +113,31 @@ class VirtualRotavap: elif not isinstance(time, (int, float)): self.logger.error(f"❌ 时间参数类型无效: {type(time)},使用默认值180.0秒") time = 180.0 - + # 确保time是float类型; 并加速 time = float(time) / 10.0 - + # 🔧 简化处理:如果vessel就是设备自己,直接操作 if vessel == self.device_id: debug_print(f"🎯 在设备 {self.device_id} 上直接执行蒸发操作") actual_vessel = self.device_id else: actual_vessel = vessel - + # 参数预处理 if solvent: self.logger.info(f"🧪 识别到溶剂: {solvent}") # 根据溶剂调整参数 solvent_lower = solvent.lower() - if any(s in solvent_lower for s in ['water', 'aqueous']): + if any(s in solvent_lower for s in ["water", "aqueous"]): temp = max(temp, 80.0) pressure = max(pressure, 0.2) self.logger.info(f"💧 水系溶剂:调整参数 → 温度 {temp}°C, 压力 {pressure} bar") - elif any(s in solvent_lower for s in ['ethanol', 'methanol', 'acetone']): + elif any(s in solvent_lower for s in ["ethanol", "methanol", "acetone"]): temp = min(temp, 50.0) pressure = min(pressure, 0.05) self.logger.info(f"⚡ 易挥发溶剂:调整参数 → 温度 {temp}°C, 压力 {pressure} bar") - + self.logger.info(f"🌪️ 开始蒸发操作: {actual_vessel}") self.logger.info(f" 🥽 容器: {actual_vessel}") self.logger.info(f" 🌡️ 温度: {temp}°C") @@ -131,126 +146,140 @@ class VirtualRotavap: self.logger.info(f" 🌀 转速: {stir_speed} RPM") if solvent: self.logger.info(f" 🧪 溶剂: {solvent}") - + # 验证参数 if temp > self._max_temp or temp < 10.0: error_msg = f"🌡️ 温度 {temp}°C 超出范围 (10-{self._max_temp}°C) ⚠️" self.logger.error(f"❌ {error_msg}") - self.data.update({ - "status": f"❌ 错误: 温度超出范围", - "rotavap_state": "Error", - "current_temp": 25.0, - "progress": 0.0, - "evaporated_volume": 0.0, - "message": error_msg - }) + self.data.update( + { + "status": f"❌ 错误: 温度超出范围", + "rotavap_state": "Error", + "current_temp": 25.0, + "progress": 0.0, + "evaporated_volume": 0.0, + "message": error_msg, + } + ) return False if stir_speed > self._max_rotation_speed or stir_speed < 10.0: error_msg = f"🌀 旋转速度 {stir_speed} RPM 超出范围 (10-{self._max_rotation_speed} RPM) ⚠️" self.logger.error(f"❌ {error_msg}") - self.data.update({ - "status": f"❌ 错误: 转速超出范围", - "rotavap_state": "Error", - "current_temp": 25.0, - "progress": 0.0, - "evaporated_volume": 0.0, - "message": error_msg - }) + self.data.update( + { + "status": f"❌ 错误: 转速超出范围", + "rotavap_state": "Error", + "current_temp": 25.0, + "progress": 0.0, + "evaporated_volume": 0.0, + "message": error_msg, + } + ) return False if pressure < 0.01 or pressure > 1.0: error_msg = f"💨 真空度 {pressure} bar 超出范围 (0.01-1.0 bar) ⚠️" self.logger.error(f"❌ {error_msg}") - self.data.update({ - "status": f"❌ 错误: 压力超出范围", - "rotavap_state": "Error", - "current_temp": 25.0, - "progress": 0.0, - "evaporated_volume": 0.0, - "message": error_msg - }) + self.data.update( + { + "status": f"❌ 错误: 压力超出范围", + "rotavap_state": "Error", + "current_temp": 25.0, + "progress": 0.0, + "evaporated_volume": 0.0, + "message": error_msg, + } + ) return False # 开始蒸发 - 🔧 现在time已经确保是float类型 self.logger.info(f"🚀 启动蒸发程序! 预计用时 {time/60:.1f}分钟 ⏱️") - - self.data.update({ - "status": f"🌪️ 蒸发中: {actual_vessel}", - "rotavap_state": "Evaporating", - "current_temp": temp, - "target_temp": temp, - "rotation_speed": stir_speed, - "vacuum_pressure": pressure, - "remaining_time": time, - "progress": 0.0, - "evaporated_volume": 0.0, - "message": f"🌪️ Evaporating {actual_vessel} at {temp}°C, {pressure} bar, {stir_speed} RPM" - }) + + self.data.update( + { + "status": f"🌪️ 蒸发中: {actual_vessel}", + "rotavap_state": "Evaporating", + "current_temp": temp, + "target_temp": temp, + "rotation_speed": stir_speed, + "vacuum_pressure": pressure, + "remaining_time": time, + "progress": 0.0, + "evaporated_volume": 0.0, + "message": f"🌪️ Evaporating {actual_vessel} at {temp}°C, {pressure} bar, {stir_speed} RPM", + } + ) try: # 蒸发过程 - 实时更新进度 start_time = time_module.time() total_time = time last_logged_progress = 0 - + while True: current_time = time_module.time() elapsed = current_time - start_time remaining = max(0, total_time - elapsed) progress = min(100.0, (elapsed / total_time) * 100) - + # 模拟蒸发体积 - 根据溶剂类型调整 - if solvent and any(s in solvent.lower() for s in ['water', 'aqueous']): + if solvent and any(s in solvent.lower() for s in ["water", "aqueous"]): evaporated_vol = progress * 0.6 # 水系溶剂蒸发慢 - elif solvent and any(s in solvent.lower() for s in ['ethanol', 'methanol', 'acetone']): + elif solvent and any(s in solvent.lower() for s in ["ethanol", "methanol", "acetone"]): evaporated_vol = progress * 1.0 # 易挥发溶剂蒸发快 else: evaporated_vol = progress * 0.8 # 默认蒸发量 - + # 🔧 更新状态 - 确保包含所有必需字段 status_msg = f"🌪️ 蒸发中: {actual_vessel} | 🌡️ {temp}°C | 💨 {pressure} bar | 🌀 {stir_speed} RPM | 📊 {progress:.1f}% | ⏰ 剩余: {remaining:.0f}s" - - self.data.update({ - "remaining_time": remaining, - "progress": progress, - "evaporated_volume": evaporated_vol, - "current_temp": temp, - "status": status_msg, - "message": f"🌪️ Evaporating: {progress:.1f}% complete, 💧 {evaporated_vol:.1f}mL evaporated, ⏰ {remaining:.0f}s remaining" - }) - + + self.data.update( + { + "remaining_time": remaining, + "progress": progress, + "evaporated_volume": evaporated_vol, + "current_temp": temp, + "status": status_msg, + "message": f"🌪️ Evaporating: {progress:.1f}% complete, 💧 {evaporated_vol:.1f}mL evaporated, ⏰ {remaining:.0f}s remaining", + } + ) + # 进度日志(每25%打印一次) if progress >= 25 and int(progress) % 25 == 0 and int(progress) != last_logged_progress: - self.logger.info(f"📊 蒸发进度: {progress:.0f}% | 💧 已蒸发: {evaporated_vol:.1f}mL | ⏰ 剩余: {remaining:.0f}s ✨") + self.logger.info( + f"📊 蒸发进度: {progress:.0f}% | 💧 已蒸发: {evaporated_vol:.1f}mL | ⏰ 剩余: {remaining:.0f}s ✨" + ) last_logged_progress = int(progress) - + # 时间到了,退出循环 if remaining <= 0: break - + # 每秒更新一次 - await asyncio.sleep(1.0) - + await self._ros_node.sleep(1.0) + # 蒸发完成 - if solvent and any(s in solvent.lower() for s in ['water', 'aqueous']): + if solvent and any(s in solvent.lower() for s in ["water", "aqueous"]): final_evaporated = 60.0 # 水系溶剂 - elif solvent and any(s in solvent.lower() for s in ['ethanol', 'methanol', 'acetone']): + elif solvent and any(s in solvent.lower() for s in ["ethanol", "methanol", "acetone"]): final_evaporated = 100.0 # 易挥发溶剂 else: final_evaporated = 80.0 # 默认 - - self.data.update({ - "status": f"✅ 蒸发完成: {actual_vessel} | 💧 蒸发量: {final_evaporated:.1f}mL", - "rotavap_state": "Completed", - "evaporated_volume": final_evaporated, - "progress": 100.0, - "current_temp": temp, - "remaining_time": 0.0, - "rotation_speed": 0.0, - "vacuum_pressure": 1.0, - "message": f"✅ Evaporation completed: {final_evaporated}mL evaporated from {actual_vessel}" - }) + + self.data.update( + { + "status": f"✅ 蒸发完成: {actual_vessel} | 💧 蒸发量: {final_evaporated:.1f}mL", + "rotavap_state": "Completed", + "evaporated_volume": final_evaporated, + "progress": 100.0, + "current_temp": temp, + "remaining_time": 0.0, + "rotation_speed": 0.0, + "vacuum_pressure": 1.0, + "message": f"✅ Evaporation completed: {final_evaporated}mL evaporated from {actual_vessel}", + } + ) self.logger.info(f"🎉 蒸发操作完成! ✨") self.logger.info(f"📊 蒸发结果:") @@ -262,24 +291,26 @@ class VirtualRotavap: self.logger.info(f" ⏱️ 总用时: {total_time:.0f}s") if solvent: self.logger.info(f" 🧪 处理溶剂: {solvent} 🏁") - + return True except Exception as e: # 出错处理 error_msg = f"蒸发过程中发生错误: {str(e)} 💥" self.logger.error(f"❌ {error_msg}") - - self.data.update({ - "status": f"❌ 蒸发错误: {str(e)}", - "rotavap_state": "Error", - "current_temp": 25.0, - "progress": 0.0, - "evaporated_volume": 0.0, - "rotation_speed": 0.0, - "vacuum_pressure": 1.0, - "message": f"❌ Evaporation failed: {str(e)}" - }) + + self.data.update( + { + "status": f"❌ 蒸发错误: {str(e)}", + "rotavap_state": "Error", + "current_temp": 25.0, + "progress": 0.0, + "evaporated_volume": 0.0, + "rotation_speed": 0.0, + "vacuum_pressure": 1.0, + "message": f"❌ Evaporation failed: {str(e)}", + } + ) return False # === 核心状态属性 === diff --git a/unilabos/devices/virtual/virtual_separator.py b/unilabos/devices/virtual/virtual_separator.py index e1c46128..0f266ce1 100644 --- a/unilabos/devices/virtual/virtual_separator.py +++ b/unilabos/devices/virtual/virtual_separator.py @@ -2,9 +2,13 @@ import asyncio import logging from typing import Dict, Any, Optional +from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode + class VirtualSeparator: """Virtual separator device for SeparateProtocol testing""" + + _ros_node: BaseROS2DeviceNode def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs): # 处理可能的不同调用方式 @@ -35,6 +39,9 @@ class VirtualSeparator: for key, value in kwargs.items(): if key not in skip_keys and not hasattr(self, key): setattr(self, key, value) + + def post_init(self, ros_node: BaseROS2DeviceNode): + self._ros_node = ros_node async def initialize(self) -> bool: """Initialize virtual separator""" @@ -119,14 +126,14 @@ class VirtualSeparator: for repeat in range(repeats): # 搅拌阶段 for progress in range(0, 51, 10): - await asyncio.sleep(simulation_time / (repeats * 10)) + await self._ros_node.sleep(simulation_time / (repeats * 10)) overall_progress = ((repeat * 100) + (progress * 0.5)) / repeats self.data["progress"] = overall_progress self.data["message"] = f"第{repeat+1}次分离 - 搅拌中 ({progress}%)" # 静置分相阶段 for progress in range(50, 101, 10): - await asyncio.sleep(simulation_time / (repeats * 10)) + await self._ros_node.sleep(simulation_time / (repeats * 10)) overall_progress = ((repeat * 100) + (progress * 0.5)) / repeats self.data["progress"] = overall_progress self.data["message"] = f"第{repeat+1}次分离 - 静置分相中 ({progress}%)" diff --git a/unilabos/devices/virtual/virtual_solenoid_valve.py b/unilabos/devices/virtual/virtual_solenoid_valve.py index e0194248..26970cbe 100644 --- a/unilabos/devices/virtual/virtual_solenoid_valve.py +++ b/unilabos/devices/virtual/virtual_solenoid_valve.py @@ -2,11 +2,16 @@ import time import asyncio from typing import Union +from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode + class VirtualSolenoidValve: """ 虚拟电磁阀门 - 简单的开关型阀门,只有开启和关闭两个状态 """ + + _ros_node: BaseROS2DeviceNode + def __init__(self, device_id: str = None, config: dict = None, **kwargs): # 从配置中获取参数,提供默认值 if config is None: @@ -21,6 +26,9 @@ class VirtualSolenoidValve: self._status = "Idle" self._valve_state = "Closed" # "Open" or "Closed" self._is_open = False + + def post_init(self, ros_node: BaseROS2DeviceNode): + self._ros_node = ros_node async def initialize(self) -> bool: """初始化设备""" @@ -63,7 +71,7 @@ class VirtualSolenoidValve: self._status = "Busy" # 模拟阀门响应时间 - await asyncio.sleep(self.response_time) + await self._ros_node.sleep(self.response_time) # 处理不同的命令格式 if isinstance(command, str): diff --git a/unilabos/devices/virtual/virtual_solid_dispenser.py b/unilabos/devices/virtual/virtual_solid_dispenser.py index f8c14a75..63182616 100644 --- a/unilabos/devices/virtual/virtual_solid_dispenser.py +++ b/unilabos/devices/virtual/virtual_solid_dispenser.py @@ -3,6 +3,8 @@ import logging import re from typing import Dict, Any, Optional +from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode + class VirtualSolidDispenser: """ 虚拟固体粉末加样器 - 用于处理 Add Protocol 中的固体试剂添加 ⚗️ @@ -13,6 +15,8 @@ class VirtualSolidDispenser: - 简单反馈:成功/失败 + 消息 📊 """ + _ros_node: BaseROS2DeviceNode + def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs): self.device_id = device_id or "virtual_solid_dispenser" self.config = config or {} @@ -32,6 +36,9 @@ class VirtualSolidDispenser: print(f"⚗️ === 虚拟固体分配器 {self.device_id} 创建成功! === ✨") print(f"📊 设备规格: 最大容量 {self.max_capacity}g | 精度 {self.precision}g 🎯") + def post_init(self, ros_node: BaseROS2DeviceNode): + self._ros_node = ros_node + async def initialize(self) -> bool: """初始化固体加样器 🚀""" self.logger.info(f"🔧 初始化固体分配器 {self.device_id} ✨") @@ -263,7 +270,7 @@ class VirtualSolidDispenser: for i in range(steps): progress = (i + 1) / steps * 100 - await asyncio.sleep(step_time) + await self._ros_node.sleep(step_time) if i % 2 == 0: # 每隔一步显示进度 self.logger.debug(f"📊 加样进度: {progress:.0f}% | {amount_emoji} 正在分配 {reagent}...") diff --git a/unilabos/devices/virtual/virtual_stirrer.py b/unilabos/devices/virtual/virtual_stirrer.py index cccf61ea..8e95617f 100644 --- a/unilabos/devices/virtual/virtual_stirrer.py +++ b/unilabos/devices/virtual/virtual_stirrer.py @@ -3,9 +3,13 @@ import logging import time as time_module from typing import Dict, Any +from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode + class VirtualStirrer: """Virtual stirrer device for StirProtocol testing - 功能完整版 🌪️""" + _ros_node: BaseROS2DeviceNode + def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs): # 处理可能的不同调用方式 if device_id is None and 'id' in kwargs: @@ -34,6 +38,9 @@ class VirtualStirrer: print(f"🌪️ === 虚拟搅拌器 {self.device_id} 已创建 === ✨") print(f"🔧 速度范围: {self._min_speed} ~ {self._max_speed} RPM | 📱 端口: {self.port}") + def post_init(self, ros_node: BaseROS2DeviceNode): + self._ros_node = ros_node + async def initialize(self) -> bool: """Initialize virtual stirrer 🚀""" self.logger.info(f"🔧 初始化虚拟搅拌器 {self.device_id} ✨") @@ -134,7 +141,7 @@ class VirtualStirrer: if remaining <= 0: break - await asyncio.sleep(1.0) + await self._ros_node.sleep(1.0) self.logger.info(f"✅ 搅拌阶段完成! 🌪️ {stir_speed} RPM × {stir_time}s") @@ -176,7 +183,7 @@ class VirtualStirrer: if remaining <= 0: break - await asyncio.sleep(1.0) + await self._ros_node.sleep(1.0) self.logger.info(f"✅ 沉降阶段完成! 🛑 静置 {settling_time}s") diff --git a/unilabos/devices/virtual/virtual_transferpump.py b/unilabos/devices/virtual/virtual_transferpump.py index 1187db5f..7b8eea86 100644 --- a/unilabos/devices/virtual/virtual_transferpump.py +++ b/unilabos/devices/virtual/virtual_transferpump.py @@ -4,6 +4,8 @@ from enum import Enum from typing import Union, Optional import logging +from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode + class VirtualPumpMode(Enum): Normal = 0 @@ -14,6 +16,8 @@ class VirtualPumpMode(Enum): class VirtualTransferPump: """虚拟转移泵类 - 模拟泵的基本功能,无需实际硬件 🚰""" + _ros_node: BaseROS2DeviceNode + def __init__(self, device_id: str = None, config: dict = None, **kwargs): """ 初始化虚拟转移泵 @@ -53,6 +57,9 @@ class VirtualTransferPump: 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}") + def post_init(self, ros_node: BaseROS2DeviceNode): + self._ros_node = ros_node + async def initialize(self) -> bool: """初始化虚拟泵 🚀""" self.logger.info(f"🔧 初始化虚拟转移泵 {self.device_id} ✨") @@ -104,7 +111,7 @@ class VirtualTransferPump: async def _simulate_operation(self, duration: float): """模拟操作延时 ⏱️""" self._status = "Busy" - await asyncio.sleep(duration) + await self._ros_node.sleep(duration) self._status = "Idle" def _calculate_duration(self, volume: float, velocity: float = None) -> float: @@ -223,7 +230,7 @@ class VirtualTransferPump: # 等待一小步时间 if i < steps and step_duration > 0: - await asyncio.sleep(step_duration) + await self._ros_node.sleep(step_duration) else: # 移动距离很小,直接完成 self._position = target_position @@ -341,7 +348,7 @@ class VirtualTransferPump: # 短暂停顿 self.logger.debug("⏸️ 短暂停顿...") - await asyncio.sleep(0.1) + await self._ros_node.sleep(0.1) # 排液 await self.dispense(volume, dispense_velocity)