mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-17 13:01:12 +00:00
Merge branch 'workstation_dev_YB3' into workstation_dev_YB3
This commit is contained in:
@@ -3,7 +3,8 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
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:
|
class SmartPumpController:
|
||||||
@@ -14,6 +15,8 @@ class SmartPumpController:
|
|||||||
适用于实验室自动化系统中的液体处理任务。
|
适用于实验室自动化系统中的液体处理任务。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, device_id: str = "smart_pump_01", port: str = "/dev/ttyUSB0"):
|
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.calibration_factor = 1.0
|
||||||
self.pump_mode = "continuous" # continuous, volume, rate
|
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:
|
def connect_device(self, timeout: int = 10) -> bool:
|
||||||
"""
|
"""
|
||||||
连接到泵设备
|
连接到泵设备
|
||||||
@@ -90,7 +96,7 @@ class SmartPumpController:
|
|||||||
pump_time = (volume / flow_rate) * 60 # 转换为秒
|
pump_time = (volume / flow_rate) * 60 # 转换为秒
|
||||||
|
|
||||||
self.current_flow_rate = flow_rate
|
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.total_volume_pumped += volume
|
||||||
self.current_flow_rate = 0.0
|
self.current_flow_rate = 0.0
|
||||||
@@ -170,6 +176,8 @@ class AdvancedTemperatureController:
|
|||||||
适用于需要精确温度控制的化学反应和材料处理过程。
|
适用于需要精确温度控制的化学反应和材料处理过程。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, controller_id: str = "temp_controller_01"):
|
def __init__(self, controller_id: str = "temp_controller_01"):
|
||||||
"""
|
"""
|
||||||
初始化温度控制器
|
初始化温度控制器
|
||||||
@@ -185,6 +193,9 @@ class AdvancedTemperatureController:
|
|||||||
self.pid_enabled = True
|
self.pid_enabled = True
|
||||||
self.temperature_history: List[Dict] = []
|
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:
|
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条
|
# 保持历史记录不超过100条
|
||||||
if len(self.temperature_history) > 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):
|
def __init__(self, analyzer_id: str = "analyzer_01", channels: int = 8):
|
||||||
"""
|
"""
|
||||||
初始化多通道分析仪
|
初始化多通道分析仪
|
||||||
@@ -344,6 +357,9 @@ class MultiChannelAnalyzer:
|
|||||||
self.is_measuring = False
|
self.is_measuring = False
|
||||||
self.sample_rate = 1000 # Hz
|
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:
|
def configure_channel(self, channel: int, enabled: bool = True, unit: str = "V") -> bool:
|
||||||
"""
|
"""
|
||||||
配置通道
|
配置通道
|
||||||
@@ -376,7 +392,7 @@ class MultiChannelAnalyzer:
|
|||||||
|
|
||||||
# 模拟数据采集
|
# 模拟数据采集
|
||||||
measurements = []
|
measurements = []
|
||||||
for second in range(duration):
|
for _ in range(duration):
|
||||||
timestamp = asyncio.get_event_loop().time()
|
timestamp = asyncio.get_event_loop().time()
|
||||||
frame_data = {}
|
frame_data = {}
|
||||||
|
|
||||||
@@ -391,7 +407,7 @@ class MultiChannelAnalyzer:
|
|||||||
|
|
||||||
measurements.append({"timestamp": timestamp, "data": frame_data})
|
measurements.append({"timestamp": timestamp, "data": frame_data})
|
||||||
|
|
||||||
await asyncio.sleep(1.0) # 每秒采集一次
|
await self._ros_node.sleep(1.0) # 每秒采集一次
|
||||||
|
|
||||||
self.is_measuring = False
|
self.is_measuring = False
|
||||||
|
|
||||||
@@ -465,6 +481,8 @@ class AutomatedDispenser:
|
|||||||
集成称重功能,确保分配精度和重现性。
|
集成称重功能,确保分配精度和重现性。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, dispenser_id: str = "dispenser_01"):
|
def __init__(self, dispenser_id: str = "dispenser_01"):
|
||||||
"""
|
"""
|
||||||
初始化自动分配器
|
初始化自动分配器
|
||||||
@@ -479,6 +497,9 @@ class AutomatedDispenser:
|
|||||||
self.container_capacity = 1000.0 # mL
|
self.container_capacity = 1000.0 # mL
|
||||||
self.precision_mode = True
|
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:
|
def move_to_position(self, x: float, y: float, z: float) -> bool:
|
||||||
"""
|
"""
|
||||||
移动到指定位置
|
移动到指定位置
|
||||||
@@ -517,7 +538,7 @@ class AutomatedDispenser:
|
|||||||
if viscosity == "high":
|
if viscosity == "high":
|
||||||
dispense_time *= 2 # 高粘度液体需要更长时间
|
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
|
self.dispensed_total += volume
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ def start_backend(
|
|||||||
graph=None,
|
graph=None,
|
||||||
controllers_config: dict = {},
|
controllers_config: dict = {},
|
||||||
bridges=[],
|
bridges=[],
|
||||||
without_host: bool = False,
|
is_slave: bool = False,
|
||||||
visual: str = "None",
|
visual: str = "None",
|
||||||
resources_mesh_config: dict = {},
|
resources_mesh_config: dict = {},
|
||||||
**kwargs,
|
**kwargs,
|
||||||
@@ -32,7 +32,7 @@ def start_backend(
|
|||||||
raise ValueError(f"Unsupported backend: {backend}")
|
raise ValueError(f"Unsupported backend: {backend}")
|
||||||
|
|
||||||
backend_thread = threading.Thread(
|
backend_thread = threading.Thread(
|
||||||
target=main if not without_host else slave,
|
target=main if not is_slave else slave,
|
||||||
args=(
|
args=(
|
||||||
devices_config,
|
devices_config,
|
||||||
resources_config,
|
resources_config,
|
||||||
|
|||||||
@@ -375,22 +375,23 @@ def main():
|
|||||||
|
|
||||||
args_dict["bridges"] = []
|
args_dict["bridges"] = []
|
||||||
|
|
||||||
# 获取通信客户端(仅支持WebSocket)
|
|
||||||
comm_client = get_communication_client()
|
|
||||||
|
|
||||||
if "websocket" in args_dict["app_bridges"]:
|
|
||||||
args_dict["bridges"].append(comm_client)
|
|
||||||
if "fastapi" in args_dict["app_bridges"]:
|
if "fastapi" in args_dict["app_bridges"]:
|
||||||
args_dict["bridges"].append(http_client)
|
args_dict["bridges"].append(http_client)
|
||||||
if "websocket" in args_dict["app_bridges"]:
|
# 获取通信客户端(仅支持WebSocket)
|
||||||
|
if BasicConfig.is_host_mode:
|
||||||
|
comm_client = get_communication_client()
|
||||||
|
if "websocket" in args_dict["app_bridges"]:
|
||||||
|
args_dict["bridges"].append(comm_client)
|
||||||
|
def _exit(signum, frame):
|
||||||
|
comm_client.stop()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
def _exit(signum, frame):
|
signal.signal(signal.SIGINT, _exit)
|
||||||
comm_client.stop()
|
signal.signal(signal.SIGTERM, _exit)
|
||||||
sys.exit(0)
|
comm_client.start()
|
||||||
|
else:
|
||||||
|
print_status("SlaveMode跳过Websocket连接")
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, _exit)
|
|
||||||
signal.signal(signal.SIGTERM, _exit)
|
|
||||||
comm_client.start()
|
|
||||||
args_dict["resources_mesh_config"] = {}
|
args_dict["resources_mesh_config"] = {}
|
||||||
args_dict["resources_edge_config"] = resource_edge_info
|
args_dict["resources_edge_config"] = resource_edge_info
|
||||||
# web visiualize 2D
|
# web visiualize 2D
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
from typing import Optional, Tuple, Dict, Any
|
||||||
|
|
||||||
from unilabos.utils.log import logger
|
from unilabos.utils.log import logger
|
||||||
from unilabos.utils.type_check import TypeEncoder
|
from unilabos.utils.type_check import TypeEncoder
|
||||||
|
|
||||||
|
|
||||||
def register_devices_and_resources(lab_registry):
|
def register_devices_and_resources(lab_registry, gather_only=False) -> Optional[Tuple[Dict[str, Any], Dict[str, Any]]]:
|
||||||
"""
|
"""
|
||||||
注册设备和资源到服务器(仅支持HTTP)
|
注册设备和资源到服务器(仅支持HTTP)
|
||||||
"""
|
"""
|
||||||
@@ -28,6 +29,8 @@ def register_devices_and_resources(lab_registry):
|
|||||||
resources_to_register[resource_info["id"]] = resource_info
|
resources_to_register[resource_info["id"]] = resource_info
|
||||||
logger.debug(f"[UniLab Register] 收集资源: {resource_info['id']}")
|
logger.debug(f"[UniLab Register] 收集资源: {resource_info['id']}")
|
||||||
|
|
||||||
|
if gather_only:
|
||||||
|
return devices_to_register, resources_to_register
|
||||||
# 注册设备
|
# 注册设备
|
||||||
if devices_to_register:
|
if devices_to_register:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from serial import Serial
|
|||||||
from serial.serialutil import SerialException
|
from serial.serialutil import SerialException
|
||||||
|
|
||||||
from unilabos.messages import Point3D
|
from unilabos.messages import Point3D
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
|
|
||||||
class GrblCNCConnectionError(Exception):
|
class GrblCNCConnectionError(Exception):
|
||||||
@@ -32,6 +33,7 @@ class GrblCNCInfo:
|
|||||||
class GrblCNCAsync:
|
class GrblCNCAsync:
|
||||||
_status: str = "Offline"
|
_status: str = "Offline"
|
||||||
_position: Point3D = Point3D(x=0.0, y=0.0, z=0.0)
|
_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)):
|
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
|
self.port = port
|
||||||
@@ -58,6 +60,9 @@ class GrblCNCAsync:
|
|||||||
self._run_future: Optional[Future[Any]] = None
|
self._run_future: Optional[Future[Any]] = None
|
||||||
self._run_lock = Lock()
|
self._run_lock = Lock()
|
||||||
|
|
||||||
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
|
self._ros_node = ros_node
|
||||||
|
|
||||||
def _read_all(self):
|
def _read_all(self):
|
||||||
data = self._serial.read_until(b"\n")
|
data = self._serial.read_until(b"\n")
|
||||||
data_decoded = data.decode()
|
data_decoded = data.decode()
|
||||||
@@ -148,7 +153,7 @@ class GrblCNCAsync:
|
|||||||
try:
|
try:
|
||||||
await self._query(command)
|
await self._query(command)
|
||||||
while True:
|
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()
|
status = await self.get_status()
|
||||||
if "Idle" in status:
|
if "Idle" in status:
|
||||||
@@ -214,7 +219,7 @@ class GrblCNCAsync:
|
|||||||
self._pose_number = i
|
self._pose_number = i
|
||||||
self.pose_number_remaining = len(points) - i
|
self.pose_number_remaining = len(points) - i
|
||||||
await self.set_position(point)
|
await self.set_position(point)
|
||||||
await asyncio.sleep(0.5)
|
await self._ros_node.sleep(0.5)
|
||||||
self._step_number = -1
|
self._step_number = -1
|
||||||
|
|
||||||
async def stop_operation(self):
|
async def stop_operation(self):
|
||||||
@@ -235,7 +240,7 @@ class GrblCNCAsync:
|
|||||||
async def open(self):
|
async def open(self):
|
||||||
if self._read_task:
|
if self._read_task:
|
||||||
raise GrblCNCConnectionError
|
raise GrblCNCConnectionError
|
||||||
self._read_task = asyncio.create_task(self._read_loop())
|
self._read_task = self._ros_node.create_task(self._read_loop())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.get_status()
|
await self.get_status()
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import time
|
|||||||
import asyncio
|
import asyncio
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
|
|
||||||
class Point3D(BaseModel):
|
class Point3D(BaseModel):
|
||||||
x: float
|
x: float
|
||||||
@@ -14,10 +16,15 @@ def d(a: Point3D, b: Point3D) -> float:
|
|||||||
|
|
||||||
|
|
||||||
class MockCNCAsync:
|
class MockCNCAsync:
|
||||||
|
_ros_node: BaseROS2DeviceNode["MockCNCAsync"]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._position: Point3D = Point3D(x=0.0, y=0.0, z=0.0)
|
self._position: Point3D = Point3D(x=0.0, y=0.0, z=0.0)
|
||||||
self._status = "Idle"
|
self._status = "Idle"
|
||||||
|
|
||||||
|
def post_create(self, ros_node):
|
||||||
|
self._ros_node = ros_node
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def position(self) -> Point3D:
|
def position(self) -> Point3D:
|
||||||
return self._position
|
return self._position
|
||||||
@@ -38,5 +45,5 @@ class MockCNCAsync:
|
|||||||
self._position.x = current_pos.x + (position.x - current_pos.x) / 20 * (i+1)
|
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.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)
|
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"
|
self._status = "Idle"
|
||||||
|
|||||||
@@ -15,9 +15,12 @@ from typing import List, Optional, Dict, Any, Union, Tuple
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
# 基础导入
|
# 基础导入
|
||||||
try:
|
try:
|
||||||
from pylabrobot.resources import Deck, Plate, TipRack, Tip, Resource, Well
|
from pylabrobot.resources import Deck, Plate, TipRack, Tip, Resource, Well
|
||||||
|
|
||||||
PYLABROBOT_AVAILABLE = True
|
PYLABROBOT_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# 如果 pylabrobot 不可用,创建基础的模拟类
|
# 如果 pylabrobot 不可用,创建基础的模拟类
|
||||||
@@ -42,17 +45,16 @@ except ImportError:
|
|||||||
class Well(Resource):
|
class Well(Resource):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# LaiYu_Liquid 控制器导入
|
# LaiYu_Liquid 控制器导入
|
||||||
try:
|
try:
|
||||||
from .controllers.pipette_controller import (
|
from .controllers.pipette_controller import PipetteController, TipStatus, LiquidClass, LiquidParameters
|
||||||
PipetteController, TipStatus, LiquidClass, LiquidParameters
|
from .controllers.xyz_controller import XYZController, MachineConfig, CoordinateOrigin, MotorAxis
|
||||||
)
|
|
||||||
from .controllers.xyz_controller import (
|
|
||||||
XYZController, MachineConfig, CoordinateOrigin, MotorAxis
|
|
||||||
)
|
|
||||||
CONTROLLERS_AVAILABLE = True
|
CONTROLLERS_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
CONTROLLERS_AVAILABLE = False
|
CONTROLLERS_AVAILABLE = False
|
||||||
|
|
||||||
# 创建模拟的控制器类
|
# 创建模拟的控制器类
|
||||||
class PipetteController:
|
class PipetteController:
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@@ -71,17 +73,20 @@ except ImportError:
|
|||||||
def connect_device(self):
|
def connect_device(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class LaiYuLiquidError(RuntimeError):
|
class LaiYuLiquidError(RuntimeError):
|
||||||
"""LaiYu_Liquid 设备异常"""
|
"""LaiYu_Liquid 设备异常"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LaiYuLiquidConfig:
|
class LaiYuLiquidConfig:
|
||||||
"""LaiYu_Liquid 设备配置"""
|
"""LaiYu_Liquid 设备配置"""
|
||||||
|
|
||||||
port: str = "/dev/cu.usbserial-3130" # RS485转USB端口
|
port: str = "/dev/cu.usbserial-3130" # RS485转USB端口
|
||||||
address: int = 1 # 设备地址
|
address: int = 1 # 设备地址
|
||||||
baudrate: int = 9600 # 波特率
|
baudrate: int = 9600 # 波特率
|
||||||
@@ -155,7 +160,17 @@ class LaiYuLiquidDeck:
|
|||||||
class LaiYuLiquidContainer:
|
class LaiYuLiquidContainer:
|
||||||
"""LaiYu_Liquid 容器类"""
|
"""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.name = name
|
||||||
self.size_x = size_x
|
self.size_x = size_x
|
||||||
self.size_y = size_y
|
self.size_y = size_y
|
||||||
@@ -197,17 +212,22 @@ class LaiYuLiquidContainer:
|
|||||||
|
|
||||||
def assign_child_resource(self, resource, location=None):
|
def assign_child_resource(self, resource, location=None):
|
||||||
"""分配子资源 - 与 PyLabRobot 资源管理系统兼容"""
|
"""分配子资源 - 与 PyLabRobot 资源管理系统兼容"""
|
||||||
if hasattr(resource, 'name'):
|
if hasattr(resource, "name"):
|
||||||
self.child_resources[resource.name] = {
|
self.child_resources[resource.name] = {"resource": resource, "location": location}
|
||||||
'resource': resource,
|
|
||||||
'location': location
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class LaiYuLiquidTipRack:
|
class LaiYuLiquidTipRack:
|
||||||
"""LaiYu_Liquid 吸头架类"""
|
"""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.name = name
|
||||||
self.size_x = size_x
|
self.size_x = size_x
|
||||||
self.size_y = size_y
|
self.size_y = size_y
|
||||||
@@ -240,10 +260,7 @@ class LaiYuLiquidTipRack:
|
|||||||
|
|
||||||
def assign_child_resource(self, resource, location=None):
|
def assign_child_resource(self, resource, location=None):
|
||||||
"""分配子资源到指定位置"""
|
"""分配子资源到指定位置"""
|
||||||
self.child_resources[resource.name] = {
|
self.child_resources[resource.name] = {"resource": resource, "location": location}
|
||||||
'resource': resource,
|
|
||||||
'location': location
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_module_info():
|
def get_module_info():
|
||||||
@@ -253,24 +270,17 @@ def get_module_info():
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "LaiYu液体处理工作站模块,提供移液器控制、XYZ轴控制和资源管理功能",
|
"description": "LaiYu液体处理工作站模块,提供移液器控制、XYZ轴控制和资源管理功能",
|
||||||
"author": "UniLabOS Team",
|
"author": "UniLabOS Team",
|
||||||
"capabilities": [
|
"capabilities": ["移液器控制", "XYZ轴运动控制", "吸头架管理", "板和容器管理", "资源位置管理"],
|
||||||
"移液器控制",
|
"dependencies": {"required": ["serial"], "optional": ["pylabrobot"]},
|
||||||
"XYZ轴运动控制",
|
|
||||||
"吸头架管理",
|
|
||||||
"板和容器管理",
|
|
||||||
"资源位置管理"
|
|
||||||
],
|
|
||||||
"dependencies": {
|
|
||||||
"required": ["serial"],
|
|
||||||
"optional": ["pylabrobot"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class LaiYuLiquidBackend:
|
class LaiYuLiquidBackend:
|
||||||
"""LaiYu_Liquid 硬件通信后端"""
|
"""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.config = config
|
||||||
self.deck = deck # 工作台引用,用于获取资源位置信息
|
self.deck = deck # 工作台引用,用于获取资源位置信息
|
||||||
self.pipette_controller = None
|
self.pipette_controller = None
|
||||||
@@ -283,6 +293,9 @@ class LaiYuLiquidBackend:
|
|||||||
self.tip_attached = False
|
self.tip_attached = False
|
||||||
self.current_volume = 0.0
|
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:
|
def _validate_position(self, x: float, y: float, z: float) -> bool:
|
||||||
"""验证位置是否在安全范围内"""
|
"""验证位置是否在安全范围内"""
|
||||||
try:
|
try:
|
||||||
@@ -348,7 +361,7 @@ class LaiYuLiquidBackend:
|
|||||||
safe_position = (
|
safe_position = (
|
||||||
self.config.deck_width / 2, # 工作台中心X
|
self.config.deck_width / 2, # 工作台中心X
|
||||||
self.config.deck_height / 2, # 工作台中心Y
|
self.config.deck_height / 2, # 工作台中心Y
|
||||||
self.config.safe_height # 安全高度Z
|
self.config.safe_height, # 安全高度Z
|
||||||
)
|
)
|
||||||
|
|
||||||
if not self._validate_position(*safe_position):
|
if not self._validate_position(*safe_position):
|
||||||
@@ -375,17 +388,12 @@ class LaiYuLiquidBackend:
|
|||||||
try:
|
try:
|
||||||
if CONTROLLERS_AVAILABLE:
|
if CONTROLLERS_AVAILABLE:
|
||||||
# 初始化移液器控制器
|
# 初始化移液器控制器
|
||||||
self.pipette_controller = PipetteController(
|
self.pipette_controller = PipetteController(port=self.config.port, address=self.config.address)
|
||||||
port=self.config.port,
|
|
||||||
address=self.config.address
|
|
||||||
)
|
|
||||||
|
|
||||||
# 初始化XYZ控制器
|
# 初始化XYZ控制器
|
||||||
machine_config = MachineConfig()
|
machine_config = MachineConfig()
|
||||||
self.xyz_controller = XYZController(
|
self.xyz_controller = XYZController(
|
||||||
port=self.config.port,
|
port=self.config.port, baudrate=self.config.baudrate, machine_config=machine_config
|
||||||
baudrate=self.config.baudrate,
|
|
||||||
machine_config=machine_config
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 连接设备
|
# 连接设备
|
||||||
@@ -412,10 +420,10 @@ class LaiYuLiquidBackend:
|
|||||||
async def stop(self):
|
async def stop(self):
|
||||||
"""停止设备"""
|
"""停止设备"""
|
||||||
try:
|
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)
|
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)
|
await asyncio.to_thread(self.xyz_controller.disconnect)
|
||||||
|
|
||||||
self.is_connected = False
|
self.is_connected = False
|
||||||
@@ -432,7 +440,7 @@ class LaiYuLiquidBackend:
|
|||||||
raise LaiYuLiquidError("设备未连接")
|
raise LaiYuLiquidError("设备未连接")
|
||||||
|
|
||||||
# 模拟移动
|
# 模拟移动
|
||||||
await asyncio.sleep(0.1) # 模拟移动时间
|
await self._ros_node.sleep(0.1) # 模拟移动时间
|
||||||
self.current_position = (x, y, z)
|
self.current_position = (x, y, z)
|
||||||
logger.debug(f"移动到位置: ({x}, {y}, {z})")
|
logger.debug(f"移动到位置: ({x}, {y}, {z})")
|
||||||
return True
|
return True
|
||||||
@@ -472,9 +480,11 @@ class LaiYuLiquidBackend:
|
|||||||
pickup_z = tip_z - self.config.tip_pickup_force_depth
|
pickup_z = tip_z - self.config.tip_pickup_force_depth
|
||||||
retract_z = tip_z + self.config.tip_pickup_retract_height
|
retract_z = tip_z + self.config.tip_pickup_retract_height
|
||||||
|
|
||||||
if not (self._validate_position(tip_x, tip_y, safe_z) and
|
if not (
|
||||||
self._validate_position(tip_x, tip_y, pickup_z) and
|
self._validate_position(tip_x, tip_y, safe_z)
|
||||||
self._validate_position(tip_x, tip_y, retract_z)):
|
and self._validate_position(tip_x, tip_y, pickup_z)
|
||||||
|
and self._validate_position(tip_x, tip_y, retract_z)
|
||||||
|
):
|
||||||
logger.error("枪头拾取位置超出安全范围")
|
logger.error("枪头拾取位置超出安全范围")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -487,8 +497,7 @@ class LaiYuLiquidBackend:
|
|||||||
safe_z = tip_z + self.config.tip_approach_height
|
safe_z = tip_z + self.config.tip_approach_height
|
||||||
logger.info(f"移动到枪头上方安全位置: ({tip_x:.2f}, {tip_y:.2f}, {safe_z:.2f})")
|
logger.info(f"移动到枪头上方安全位置: ({tip_x:.2f}, {tip_y:.2f}, {safe_z:.2f})")
|
||||||
move_success = await asyncio.to_thread(
|
move_success = await asyncio.to_thread(
|
||||||
self.xyz_controller.move_to_work_coord,
|
self.xyz_controller.move_to_work_coord, tip_x, tip_y, safe_z
|
||||||
tip_x, tip_y, safe_z
|
|
||||||
)
|
)
|
||||||
if not move_success:
|
if not move_success:
|
||||||
logger.error("移动到枪头上方失败")
|
logger.error("移动到枪头上方失败")
|
||||||
@@ -498,22 +507,20 @@ class LaiYuLiquidBackend:
|
|||||||
pickup_z = tip_z - self.config.tip_pickup_force_depth
|
pickup_z = tip_z - self.config.tip_pickup_force_depth
|
||||||
logger.info(f"Z轴下降到枪头拾取位置: {pickup_z:.2f}mm")
|
logger.info(f"Z轴下降到枪头拾取位置: {pickup_z:.2f}mm")
|
||||||
z_down_success = await asyncio.to_thread(
|
z_down_success = await asyncio.to_thread(
|
||||||
self.xyz_controller.move_to_work_coord,
|
self.xyz_controller.move_to_work_coord, tip_x, tip_y, pickup_z
|
||||||
tip_x, tip_y, pickup_z
|
|
||||||
)
|
)
|
||||||
if not z_down_success:
|
if not z_down_success:
|
||||||
logger.error("Z轴下降到枪头位置失败")
|
logger.error("Z轴下降到枪头位置失败")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 3. 等待一小段时间确保枪头牢固附着
|
# 3. 等待一小段时间确保枪头牢固附着
|
||||||
await asyncio.sleep(0.2)
|
await self._ros_node.sleep(0.2)
|
||||||
|
|
||||||
# 4. Z轴上升到回退高度
|
# 4. Z轴上升到回退高度
|
||||||
retract_z = tip_z + self.config.tip_pickup_retract_height
|
retract_z = tip_z + self.config.tip_pickup_retract_height
|
||||||
logger.info(f"Z轴上升到回退高度: {retract_z:.2f}mm")
|
logger.info(f"Z轴上升到回退高度: {retract_z:.2f}mm")
|
||||||
z_up_success = await asyncio.to_thread(
|
z_up_success = await asyncio.to_thread(
|
||||||
self.xyz_controller.move_to_work_coord,
|
self.xyz_controller.move_to_work_coord, tip_x, tip_y, retract_z
|
||||||
tip_x, tip_y, retract_z
|
|
||||||
)
|
)
|
||||||
if not z_up_success:
|
if not z_up_success:
|
||||||
logger.error("Z轴上升失败")
|
logger.error("Z轴上升失败")
|
||||||
@@ -533,7 +540,7 @@ class LaiYuLiquidBackend:
|
|||||||
else:
|
else:
|
||||||
# 模拟模式
|
# 模拟模式
|
||||||
logger.info("模拟模式:执行枪头拾取动作")
|
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)
|
self.current_position = (tip_x, tip_y, tip_z + self.config.tip_pickup_retract_height)
|
||||||
|
|
||||||
# 6. 标记枪头已附着
|
# 6. 标记枪头已附着
|
||||||
@@ -578,8 +585,10 @@ class LaiYuLiquidBackend:
|
|||||||
safe_z = drop_z + self.config.safe_height
|
safe_z = drop_z + self.config.safe_height
|
||||||
drop_height_z = drop_z + self.config.tip_drop_height
|
drop_height_z = drop_z + self.config.tip_drop_height
|
||||||
|
|
||||||
if not (self._validate_position(drop_x, drop_y, safe_z) and
|
if not (
|
||||||
self._validate_position(drop_x, drop_y, drop_height_z)):
|
self._validate_position(drop_x, drop_y, safe_z)
|
||||||
|
and self._validate_position(drop_x, drop_y, drop_height_z)
|
||||||
|
):
|
||||||
logger.error("枪头丢弃位置超出安全范围")
|
logger.error("枪头丢弃位置超出安全范围")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -592,8 +601,7 @@ class LaiYuLiquidBackend:
|
|||||||
safe_z = drop_z + self.config.tip_drop_height
|
safe_z = drop_z + self.config.tip_drop_height
|
||||||
logger.info(f"移动到丢弃位置上方: ({drop_x:.2f}, {drop_y:.2f}, {safe_z:.2f})")
|
logger.info(f"移动到丢弃位置上方: ({drop_x:.2f}, {drop_y:.2f}, {safe_z:.2f})")
|
||||||
move_success = await asyncio.to_thread(
|
move_success = await asyncio.to_thread(
|
||||||
self.xyz_controller.move_to_work_coord,
|
self.xyz_controller.move_to_work_coord, drop_x, drop_y, safe_z
|
||||||
drop_x, drop_y, safe_z
|
|
||||||
)
|
)
|
||||||
if not move_success:
|
if not move_success:
|
||||||
logger.error("移动到丢弃位置上方失败")
|
logger.error("移动到丢弃位置上方失败")
|
||||||
@@ -602,8 +610,7 @@ class LaiYuLiquidBackend:
|
|||||||
# 2. Z轴下降到丢弃高度
|
# 2. Z轴下降到丢弃高度
|
||||||
logger.info(f"Z轴下降到丢弃高度: {drop_z:.2f}mm")
|
logger.info(f"Z轴下降到丢弃高度: {drop_z:.2f}mm")
|
||||||
z_down_success = await asyncio.to_thread(
|
z_down_success = await asyncio.to_thread(
|
||||||
self.xyz_controller.move_to_work_coord,
|
self.xyz_controller.move_to_work_coord, drop_x, drop_y, drop_z
|
||||||
drop_x, drop_y, drop_z
|
|
||||||
)
|
)
|
||||||
if not z_down_success:
|
if not z_down_success:
|
||||||
logger.error("Z轴下降到丢弃位置失败")
|
logger.error("Z轴下降到丢弃位置失败")
|
||||||
@@ -619,13 +626,12 @@ class LaiYuLiquidBackend:
|
|||||||
logger.warning(f"枪头弹出命令失败: {e}")
|
logger.warning(f"枪头弹出命令失败: {e}")
|
||||||
|
|
||||||
# 4. 等待一小段时间确保枪头完全脱离
|
# 4. 等待一小段时间确保枪头完全脱离
|
||||||
await asyncio.sleep(0.3)
|
await self._ros_node.sleep(0.3)
|
||||||
|
|
||||||
# 5. Z轴上升到安全高度
|
# 5. Z轴上升到安全高度
|
||||||
logger.info(f"Z轴上升到安全高度: {safe_z:.2f}mm")
|
logger.info(f"Z轴上升到安全高度: {safe_z:.2f}mm")
|
||||||
z_up_success = await asyncio.to_thread(
|
z_up_success = await asyncio.to_thread(
|
||||||
self.xyz_controller.move_to_work_coord,
|
self.xyz_controller.move_to_work_coord, drop_x, drop_y, safe_z
|
||||||
drop_x, drop_y, safe_z
|
|
||||||
)
|
)
|
||||||
if not z_up_success:
|
if not z_up_success:
|
||||||
logger.error("Z轴上升失败")
|
logger.error("Z轴上升失败")
|
||||||
@@ -645,7 +651,7 @@ class LaiYuLiquidBackend:
|
|||||||
else:
|
else:
|
||||||
# 模拟模式
|
# 模拟模式
|
||||||
logger.info("模拟模式:执行枪头丢弃动作")
|
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)
|
self.current_position = (drop_x, drop_y, drop_z + self.config.tip_drop_height)
|
||||||
|
|
||||||
# 7. 标记枪头已脱离,清空体积
|
# 7. 标记枪头已脱离,清空体积
|
||||||
@@ -671,7 +677,7 @@ class LaiYuLiquidBackend:
|
|||||||
raise LaiYuLiquidError(f"体积超出范围: {volume}")
|
raise LaiYuLiquidError(f"体积超出范围: {volume}")
|
||||||
|
|
||||||
# 模拟吸取
|
# 模拟吸取
|
||||||
await asyncio.sleep(0.3)
|
await self._ros_node.sleep(0.3)
|
||||||
self.current_volume += volume
|
self.current_volume += volume
|
||||||
logger.debug(f"从 {location} 吸取 {volume} μL")
|
logger.debug(f"从 {location} 吸取 {volume} μL")
|
||||||
return True
|
return True
|
||||||
@@ -693,7 +699,7 @@ class LaiYuLiquidBackend:
|
|||||||
raise LaiYuLiquidError(f"分配体积无效: {volume}")
|
raise LaiYuLiquidError(f"分配体积无效: {volume}")
|
||||||
|
|
||||||
# 模拟分配
|
# 模拟分配
|
||||||
await asyncio.sleep(0.3)
|
await self._ros_node.sleep(0.3)
|
||||||
self.current_volume -= volume
|
self.current_volume -= volume
|
||||||
logger.debug(f"向 {location} 分配 {volume} μL")
|
logger.debug(f"向 {location} 分配 {volume} μL")
|
||||||
return True
|
return True
|
||||||
@@ -765,8 +771,9 @@ class LaiYuLiquid:
|
|||||||
await self.backend.stop()
|
await self.backend.stop()
|
||||||
self.is_setup = False
|
self.is_setup = False
|
||||||
|
|
||||||
async def transfer(self, source: str, target: str, volume: float,
|
async def transfer(
|
||||||
tip_rack: str = "tip_rack_1", tip_position: int = 0) -> bool:
|
self, source: str, target: str, volume: float, tip_rack: str = "tip_rack_1", tip_position: int = 0
|
||||||
|
) -> bool:
|
||||||
"""液体转移"""
|
"""液体转移"""
|
||||||
try:
|
try:
|
||||||
if not self.is_setup:
|
if not self.is_setup:
|
||||||
@@ -788,7 +795,7 @@ class LaiYuLiquid:
|
|||||||
("吸取液体", self.backend.aspirate(volume, source)),
|
("吸取液体", self.backend.aspirate(volume, source)),
|
||||||
("移动到目标位置", self.backend.move_to(*target_pos)),
|
("移动到目标位置", self.backend.move_to(*target_pos)),
|
||||||
("分配液体", self.backend.dispense(volume, target)),
|
("分配液体", self.backend.dispense(volume, target)),
|
||||||
("丢弃吸头", self.backend.drop_tip())
|
("丢弃吸头", self.backend.drop_tip()),
|
||||||
]
|
]
|
||||||
|
|
||||||
for step_name, step_coro in steps:
|
for step_name, step_coro in steps:
|
||||||
@@ -823,7 +830,7 @@ class LaiYuLiquid:
|
|||||||
"current_position": self.backend.current_position,
|
"current_position": self.backend.current_position,
|
||||||
"tip_attached": self.backend.tip_attached,
|
"tip_attached": self.backend.tip_attached,
|
||||||
"current_volume": self.backend.current_volume,
|
"current_volume": self.backend.current_volume,
|
||||||
"resources": self.deck.list_resources()
|
"resources": self.deck.list_resources(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -846,7 +853,7 @@ def create_quick_setup() -> LaiYuLiquidDeck:
|
|||||||
create_tip_rack_1000ul,
|
create_tip_rack_1000ul,
|
||||||
create_tip_rack_200ul,
|
create_tip_rack_200ul,
|
||||||
create_96_well_plate,
|
create_96_well_plate,
|
||||||
create_waste_container
|
create_waste_container,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 添加基本资源
|
# 添加基本资源
|
||||||
@@ -877,5 +884,5 @@ __all__ = [
|
|||||||
"LaiYuLiquidTipRack",
|
"LaiYuLiquidTipRack",
|
||||||
"LaiYuLiquidError",
|
"LaiYuLiquidError",
|
||||||
"create_quick_setup",
|
"create_quick_setup",
|
||||||
"get_module_info"
|
"get_module_info",
|
||||||
]
|
]
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import re
|
|
||||||
import traceback
|
|
||||||
from typing import List, Sequence, Optional, Literal, Union, Iterator, Dict, Any, Callable, Set, cast
|
|
||||||
from collections import Counter
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import time
|
import time
|
||||||
import pprint as pp
|
import traceback
|
||||||
|
from collections import Counter
|
||||||
|
from typing import List, Sequence, Optional, Literal, Union, Iterator, Dict, Any, Callable, Set, cast
|
||||||
|
|
||||||
from pylabrobot.liquid_handling import LiquidHandler, LiquidHandlerBackend, LiquidHandlerChatterboxBackend, Strictness
|
from pylabrobot.liquid_handling import LiquidHandler, LiquidHandlerBackend, LiquidHandlerChatterboxBackend, Strictness
|
||||||
from pylabrobot.liquid_handling.liquid_handler import TipPresenceProbingMethod
|
from pylabrobot.liquid_handling.liquid_handler import TipPresenceProbingMethod
|
||||||
from pylabrobot.liquid_handling.standard import GripDirection
|
from pylabrobot.liquid_handling.standard import GripDirection
|
||||||
@@ -25,6 +25,8 @@ from pylabrobot.resources import (
|
|||||||
Tip,
|
Tip,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
|
|
||||||
class LiquidHandlerMiddleware(LiquidHandler):
|
class LiquidHandlerMiddleware(LiquidHandler):
|
||||||
def __init__(self, backend: LiquidHandlerBackend, deck: Deck, simulator: bool = False, channel_num: int = 8):
|
def __init__(self, backend: LiquidHandlerBackend, deck: Deck, simulator: bool = False, channel_num: int = 8):
|
||||||
@@ -536,6 +538,7 @@ class LiquidHandlerMiddleware(LiquidHandler):
|
|||||||
class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
||||||
"""Extended LiquidHandler with additional operations."""
|
"""Extended LiquidHandler with additional operations."""
|
||||||
support_touch_tip = True
|
support_touch_tip = True
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, backend: LiquidHandlerBackend, deck: Deck, simulator: bool=False, channel_num:int = 8):
|
def __init__(self, backend: LiquidHandlerBackend, deck: Deck, simulator: bool=False, channel_num:int = 8):
|
||||||
"""Initialize a LiquidHandler.
|
"""Initialize a LiquidHandler.
|
||||||
@@ -548,8 +551,11 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
self.group_info = dict()
|
self.group_info = dict()
|
||||||
super().__init__(backend, deck, simulator, channel_num)
|
super().__init__(backend, deck, simulator, channel_num)
|
||||||
|
|
||||||
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
|
self._ros_node = ros_node
|
||||||
|
|
||||||
@classmethod
|
@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."""
|
"""Set the liquid in a well."""
|
||||||
for well, liquid_name, volume in zip(wells, liquid_names, volumes):
|
for well, liquid_name, volume in zip(wells, liquid_names, volumes):
|
||||||
well.set_liquids([(liquid_name, volume)]) # type: ignore
|
well.set_liquids([(liquid_name, volume)]) # type: ignore
|
||||||
@@ -1081,7 +1087,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
|||||||
print(f"Waiting time: {msg}")
|
print(f"Waiting time: {msg}")
|
||||||
print(f"Current time: {time.strftime('%H:%M:%S')}")
|
print(f"Current time: {time.strftime('%H:%M:%S')}")
|
||||||
print(f"Time to finish: {time.strftime('%H:%M:%S', time.localtime(time.time() + seconds))}")
|
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:
|
if msg:
|
||||||
print(f"Done: {msg}")
|
print(f"Done: {msg}")
|
||||||
print(f"Current time: {time.strftime('%H:%M:%S')}")
|
print(f"Current time: {time.strftime('%H:%M:%S')}")
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ from pylabrobot.liquid_handling.standard import (
|
|||||||
from pylabrobot.resources import Tip, Deck, Plate, Well, TipRack, Resource, Container, Coordinate, TipSpot, Trash
|
from pylabrobot.resources import Tip, Deck, Plate, Well, TipRack, Resource, Container, Coordinate, TipSpot, Trash
|
||||||
|
|
||||||
from unilabos.devices.liquid_handling.liquid_handler_abstract import LiquidHandlerAbstract
|
from unilabos.devices.liquid_handling.liquid_handler_abstract import LiquidHandlerAbstract
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
|
|
||||||
class PRCXIError(RuntimeError):
|
class PRCXIError(RuntimeError):
|
||||||
@@ -162,6 +163,10 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
|||||||
)
|
)
|
||||||
super().__init__(backend=self._unilabos_backend, deck=deck, simulator=simulator, channel_num=channel_num)
|
super().__init__(backend=self._unilabos_backend, deck=deck, simulator=simulator, channel_num=channel_num)
|
||||||
|
|
||||||
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
|
super().post_init(ros_node)
|
||||||
|
self._unilabos_backend.post_init(ros_node)
|
||||||
|
|
||||||
def set_liquid(self, wells: list[Well], liquid_names: list[str], volumes: list[float]):
|
def set_liquid(self, wells: list[Well], liquid_names: list[str], volumes: list[float]):
|
||||||
return super().set_liquid(wells, liquid_names, volumes)
|
return super().set_liquid(wells, liquid_names, volumes)
|
||||||
|
|
||||||
@@ -424,6 +429,7 @@ class PRCXI9300Backend(LiquidHandlerBackend):
|
|||||||
|
|
||||||
_num_channels = 8 # 默认通道数为 8
|
_num_channels = 8 # 默认通道数为 8
|
||||||
_is_reset_ok = False
|
_is_reset_ok = False
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_reset_ok(self) -> bool:
|
def is_reset_ok(self) -> bool:
|
||||||
@@ -456,6 +462,9 @@ class PRCXI9300Backend(LiquidHandlerBackend):
|
|||||||
self._execute_setup = setup
|
self._execute_setup = setup
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
|
|
||||||
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
|
self._ros_node = ros_node
|
||||||
|
|
||||||
def create_protocol(self, protocol_name):
|
def create_protocol(self, protocol_name):
|
||||||
self.protocol_name = protocol_name
|
self.protocol_name = protocol_name
|
||||||
self.steps_todo_list = []
|
self.steps_todo_list = []
|
||||||
@@ -500,7 +509,7 @@ class PRCXI9300Backend(LiquidHandlerBackend):
|
|||||||
self.api_client.call("IAutomation", "Reset")
|
self.api_client.call("IAutomation", "Reset")
|
||||||
while not self.is_reset_ok:
|
while not self.is_reset_ok:
|
||||||
print("Waiting for PRCXI9300 to reset...")
|
print("Waiting for PRCXI9300 to reset...")
|
||||||
await asyncio.sleep(1)
|
await self._ros_node.sleep(1)
|
||||||
print("PRCXI9300 reset successfully.")
|
print("PRCXI9300 reset successfully.")
|
||||||
except ConnectionRefusedError as e:
|
except ConnectionRefusedError as e:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
@@ -533,7 +542,9 @@ class PRCXI9300Backend(LiquidHandlerBackend):
|
|||||||
tipspot_index = tipspot.parent.children.index(tipspot)
|
tipspot_index = tipspot.parent.children.index(tipspot)
|
||||||
tip_columns.append(tipspot_index // 8)
|
tip_columns.append(tipspot_index // 8)
|
||||||
if len(set(tip_columns)) != 1:
|
if len(set(tip_columns)) != 1:
|
||||||
raise ValueError("All pickups must be from the same tip column. Found different columns: " + str(tip_columns))
|
raise ValueError(
|
||||||
|
"All pickups must be from the same tip column. Found different columns: " + str(tip_columns)
|
||||||
|
)
|
||||||
PlateNo = plate_indexes[0] + 1
|
PlateNo = plate_indexes[0] + 1
|
||||||
hole_col = tip_columns[0] + 1
|
hole_col = tip_columns[0] + 1
|
||||||
hole_row = 1
|
hole_row = 1
|
||||||
@@ -1109,12 +1120,15 @@ class PRCXI9300Api:
|
|||||||
"LiquidDispensingMethod": liquid_method,
|
"LiquidDispensingMethod": liquid_method,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DefaultLayout:
|
class DefaultLayout:
|
||||||
|
|
||||||
def __init__(self, product_name: str = "PRCXI9300"):
|
def __init__(self, product_name: str = "PRCXI9300"):
|
||||||
self.labresource = {}
|
self.labresource = {}
|
||||||
if product_name not in ["PRCXI9300", "PRCXI9320"]:
|
if product_name not in ["PRCXI9300", "PRCXI9320"]:
|
||||||
raise ValueError(f"Unsupported product_name: {product_name}. Only 'PRCXI9300' and 'PRCXI9320' are supported.")
|
raise ValueError(
|
||||||
|
f"Unsupported product_name: {product_name}. Only 'PRCXI9300' and 'PRCXI9320' are supported."
|
||||||
|
)
|
||||||
|
|
||||||
if product_name == "PRCXI9300":
|
if product_name == "PRCXI9300":
|
||||||
self.rows = 2
|
self.rows = 2
|
||||||
@@ -1129,25 +1143,93 @@ class DefaultLayout:
|
|||||||
self.layout = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
|
self.layout = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
|
||||||
self.trash_slot = 16
|
self.trash_slot = 16
|
||||||
self.waste_liquid_slot = 12
|
self.waste_liquid_slot = 12
|
||||||
self.default_layout = {"MatrixId":f"{time.time()}","MatrixName":f"{time.time()}","MatrixCount":16,"WorkTablets":
|
self.default_layout = {
|
||||||
[{"Number": 1, "Code": "T1", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
"MatrixId": f"{time.time()}",
|
||||||
{"Number": 2, "Code": "T2", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
"MatrixName": f"{time.time()}",
|
||||||
{"Number": 3, "Code": "T3", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
"MatrixCount": 16,
|
||||||
{"Number": 4, "Code": "T4", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
"WorkTablets": [
|
||||||
{"Number": 5, "Code": "T5", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
{
|
||||||
{"Number": 6, "Code": "T6", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
"Number": 1,
|
||||||
{"Number": 7, "Code": "T7", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
"Code": "T1",
|
||||||
{"Number": 8, "Code": "T8", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
{"Number": 9, "Code": "T9", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
},
|
||||||
{"Number": 10, "Code": "T10", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
{
|
||||||
{"Number": 11, "Code": "T11", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
"Number": 2,
|
||||||
{"Number": 12, "Code": "T12", "Material": {"uuid": "730067cf07ae43849ddf4034299030e9", "materialEnum": 0}}, # 这个设置成废液槽,用储液槽表示
|
"Code": "T2",
|
||||||
{"Number": 13, "Code": "T13", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
{"Number": 14, "Code": "T14", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
},
|
||||||
{"Number": 15, "Code": "T15", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
|
{
|
||||||
{"Number": 16, "Code": "T16", "Material": {"uuid": "730067cf07ae43849ddf4034299030e9", "materialEnum": 0}} # 这个设置成垃圾桶,用储液槽表示
|
"Number": 3,
|
||||||
]
|
"Code": "T3",
|
||||||
}
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Number": 4,
|
||||||
|
"Code": "T4",
|
||||||
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Number": 5,
|
||||||
|
"Code": "T5",
|
||||||
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Number": 6,
|
||||||
|
"Code": "T6",
|
||||||
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Number": 7,
|
||||||
|
"Code": "T7",
|
||||||
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Number": 8,
|
||||||
|
"Code": "T8",
|
||||||
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Number": 9,
|
||||||
|
"Code": "T9",
|
||||||
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Number": 10,
|
||||||
|
"Code": "T10",
|
||||||
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Number": 11,
|
||||||
|
"Code": "T11",
|
||||||
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Number": 12,
|
||||||
|
"Code": "T12",
|
||||||
|
"Material": {"uuid": "730067cf07ae43849ddf4034299030e9", "materialEnum": 0},
|
||||||
|
}, # 这个设置成废液槽,用储液槽表示
|
||||||
|
{
|
||||||
|
"Number": 13,
|
||||||
|
"Code": "T13",
|
||||||
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Number": 14,
|
||||||
|
"Code": "T14",
|
||||||
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Number": 15,
|
||||||
|
"Code": "T15",
|
||||||
|
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Number": 16,
|
||||||
|
"Code": "T16",
|
||||||
|
"Material": {"uuid": "730067cf07ae43849ddf4034299030e9", "materialEnum": 0},
|
||||||
|
}, # 这个设置成垃圾桶,用储液槽表示
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
def get_layout(self) -> Dict[str, Any]:
|
def get_layout(self) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
@@ -1155,7 +1237,7 @@ class DefaultLayout:
|
|||||||
"columns": self.columns,
|
"columns": self.columns,
|
||||||
"layout": self.layout,
|
"layout": self.layout,
|
||||||
"trash_slot": self.trash_slot,
|
"trash_slot": self.trash_slot,
|
||||||
"waste_liquid_slot": self.waste_liquid_slot
|
"waste_liquid_slot": self.waste_liquid_slot,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_trash_slot(self) -> int:
|
def get_trash_slot(self) -> int:
|
||||||
@@ -1178,17 +1260,19 @@ class DefaultLayout:
|
|||||||
reserved_positions = {12, 16}
|
reserved_positions = {12, 16}
|
||||||
available_positions = [i for i in range(1, 17) if i not in reserved_positions]
|
available_positions = [i for i in range(1, 17) if i not in reserved_positions]
|
||||||
|
|
||||||
# 计算总需求
|
# 计算总需求
|
||||||
total_needed = sum(count for _, _, count in needs)
|
total_needed = sum(count for _, _, count in needs)
|
||||||
if total_needed > len(available_positions):
|
if total_needed > len(available_positions):
|
||||||
raise ValueError(f"需要 {total_needed} 个位置,但只有 {len(available_positions)} 个可用位置(排除位置12和16)")
|
raise ValueError(
|
||||||
|
f"需要 {total_needed} 个位置,但只有 {len(available_positions)} 个可用位置(排除位置12和16)"
|
||||||
|
)
|
||||||
|
|
||||||
# 依次分配位置
|
# 依次分配位置
|
||||||
current_pos = 0
|
current_pos = 0
|
||||||
for reagent_name, material_name, count in needs:
|
for reagent_name, material_name, count in needs:
|
||||||
|
|
||||||
material_uuid = self.labresource[material_name]['uuid']
|
material_uuid = self.labresource[material_name]["uuid"]
|
||||||
material_enum = self.labresource[material_name]['materialEnum']
|
material_enum = self.labresource[material_name]["materialEnum"]
|
||||||
|
|
||||||
for _ in range(count):
|
for _ in range(count):
|
||||||
if current_pos >= len(available_positions):
|
if current_pos >= len(available_positions):
|
||||||
@@ -1196,17 +1280,18 @@ class DefaultLayout:
|
|||||||
|
|
||||||
position = available_positions[current_pos]
|
position = available_positions[current_pos]
|
||||||
# 找到对应的tablet并更新
|
# 找到对应的tablet并更新
|
||||||
for tablet in self.default_layout['WorkTablets']:
|
for tablet in self.default_layout["WorkTablets"]:
|
||||||
if tablet['Number'] == position:
|
if tablet["Number"] == position:
|
||||||
tablet['Material']['uuid'] = material_uuid
|
tablet["Material"]["uuid"] = material_uuid
|
||||||
tablet['Material']['materialEnum'] = material_enum
|
tablet["Material"]["materialEnum"] = material_enum
|
||||||
layout_list.append(dict(reagent_name=reagent_name, material_name=material_name, positions=position))
|
layout_list.append(
|
||||||
|
dict(reagent_name=reagent_name, material_name=material_name, positions=position)
|
||||||
|
)
|
||||||
break
|
break
|
||||||
current_pos += 1
|
current_pos += 1
|
||||||
return self.default_layout, layout_list
|
return self.default_layout, layout_list
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Example usage
|
# Example usage
|
||||||
# 1. 用导出的json,给每个T1 T2板子设定相应的物料,如果是孔板和枪头盒,要对应区分
|
# 1. 用导出的json,给每个T1 T2板子设定相应的物料,如果是孔板和枪头盒,要对应区分
|
||||||
@@ -1302,9 +1387,6 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
# # # plate2.set_well_liquids(plate_2_liquids)
|
# # # plate2.set_well_liquids(plate_2_liquids)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# handler = PRCXI9300Handler(deck=deck, host="10.181.214.132", port=9999,
|
# handler = PRCXI9300Handler(deck=deck, host="10.181.214.132", port=9999,
|
||||||
# timeout=10.0, setup=False, debug=False,
|
# timeout=10.0, setup=False, debug=False,
|
||||||
# simulator=True,
|
# simulator=True,
|
||||||
@@ -1391,10 +1473,7 @@ if __name__ == "__main__":
|
|||||||
# # input("Press Enter to continue...") # Wait for user input before proceeding
|
# # input("Press Enter to continue...") # Wait for user input before proceeding
|
||||||
# # print("PRCXI9300Handler initialized with deck and host settings.")
|
# # print("PRCXI9300Handler initialized with deck and host settings.")
|
||||||
|
|
||||||
|
### 9320 ###
|
||||||
|
|
||||||
### 9320 ###
|
|
||||||
|
|
||||||
|
|
||||||
deck = PRCXI9300Deck(name="PRCXI_Deck", size_x=100, size_y=100, size_z=100)
|
deck = PRCXI9300Deck(name="PRCXI_Deck", size_x=100, size_y=100, size_z=100)
|
||||||
|
|
||||||
@@ -1412,12 +1491,15 @@ if __name__ == "__main__":
|
|||||||
new_plate: PRCXI9300Container = PRCXI9300Container.deserialize(well_containers)
|
new_plate: PRCXI9300Container = PRCXI9300Container.deserialize(well_containers)
|
||||||
return new_plate
|
return new_plate
|
||||||
|
|
||||||
def get_tip_rack(name: str, child_prefix: str="tip") -> PRCXI9300Container:
|
def get_tip_rack(name: str, child_prefix: str = "tip") -> PRCXI9300Container:
|
||||||
tip_racks = opentrons_96_tiprack_10ul(name).serialize()
|
tip_racks = opentrons_96_tiprack_10ul(name).serialize()
|
||||||
tip_rack = PRCXI9300Container(
|
tip_rack = PRCXI9300Container(
|
||||||
name=name, size_x=50, size_y=50, size_z=10, category="tip_rack", ordering=collections.OrderedDict({
|
name=name,
|
||||||
k: f"{child_prefix}_{k}" for k, v in tip_racks["ordering"].items()
|
size_x=50,
|
||||||
})
|
size_y=50,
|
||||||
|
size_z=10,
|
||||||
|
category="tip_rack",
|
||||||
|
ordering=collections.OrderedDict({k: f"{child_prefix}_{k}" for k, v in tip_racks["ordering"].items()}),
|
||||||
)
|
)
|
||||||
tip_rack_serialized = tip_rack.serialize()
|
tip_rack_serialized = tip_rack.serialize()
|
||||||
tip_rack_serialized["parent_name"] = deck.name
|
tip_rack_serialized["parent_name"] = deck.name
|
||||||
@@ -1629,6 +1711,7 @@ if __name__ == "__main__":
|
|||||||
)
|
)
|
||||||
backend: PRCXI9300Backend = handler.backend
|
backend: PRCXI9300Backend = handler.backend
|
||||||
from pylabrobot.resources import set_volume_tracking
|
from pylabrobot.resources import set_volume_tracking
|
||||||
|
|
||||||
set_volume_tracking(enabled=True)
|
set_volume_tracking(enabled=True)
|
||||||
# res = backend.api_client.get_all_materials()
|
# res = backend.api_client.get_all_materials()
|
||||||
asyncio.run(handler.setup()) # Initialize the handler and setup the connection
|
asyncio.run(handler.setup()) # Initialize the handler and setup the connection
|
||||||
@@ -1641,7 +1724,7 @@ if __name__ == "__main__":
|
|||||||
for well in plate13.get_all_items():
|
for well in plate13.get_all_items():
|
||||||
# well_pos = well.name.split("_")[1] # 走一行
|
# well_pos = well.name.split("_")[1] # 走一行
|
||||||
# if well_pos.startswith("A"):
|
# if well_pos.startswith("A"):
|
||||||
if well.name.startswith("PlateT13"): # 走整个Plate
|
if well.name.startswith("PlateT13"): # 走整个Plate
|
||||||
asyncio.run(handler.dispense([well], [0.01], [0]))
|
asyncio.run(handler.dispense([well], [0.01], [0]))
|
||||||
|
|
||||||
# asyncio.run(handler.dispense([plate10.get_item("H12")], [1], [0]))
|
# asyncio.run(handler.dispense([plate10.get_item("H12")], [1], [0]))
|
||||||
@@ -1652,26 +1735,25 @@ if __name__ == "__main__":
|
|||||||
asyncio.run(handler.run_protocol())
|
asyncio.run(handler.run_protocol())
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
os._exit(0)
|
os._exit(0)
|
||||||
# 第一种情景:一个孔往多个孔加液
|
# 第一种情景:一个孔往多个孔加液
|
||||||
# plate_2_liquids = handler.set_group("water", [plate2.children[0]], [300])
|
# plate_2_liquids = handler.set_group("water", [plate2.children[0]], [300])
|
||||||
# plate5_liquids = handler.set_group("master_mix", plate5.children[:23], [100]*23)
|
# plate5_liquids = handler.set_group("master_mix", plate5.children[:23], [100]*23)
|
||||||
# 第二个情景:多个孔往多个孔加液(但是个数得对应)
|
# 第二个情景:多个孔往多个孔加液(但是个数得对应)
|
||||||
plate_2_liquids = handler.set_group("water", plate2.children[:23], [300]*23)
|
plate_2_liquids = handler.set_group("water", plate2.children[:23], [300] * 23)
|
||||||
plate5_liquids = handler.set_group("master_mix", plate5.children[:23], [100]*23)
|
plate5_liquids = handler.set_group("master_mix", plate5.children[:23], [100] * 23)
|
||||||
|
|
||||||
# plate11.set_well_liquids([("Water", 100) if (i % 8 == 0 and i // 8 < 6) else (None, 100) for i in range(96)]) # Set liquids for every 8 wells in plate8
|
# plate11.set_well_liquids([("Water", 100) if (i % 8 == 0 and i // 8 < 6) else (None, 100) for i in range(96)]) # Set liquids for every 8 wells in plate8
|
||||||
|
|
||||||
# plate11.set_well_liquids([("Water", 100) if (i % 8 == 0 and i // 8 < 6) else (None, 100) for i in range(96)]) # Set liquids for every 8 wells in plate8
|
# plate11.set_well_liquids([("Water", 100) if (i % 8 == 0 and i // 8 < 6) else (None, 100) for i in range(96)]) # Set liquids for every 8 wells in plate8
|
||||||
|
|
||||||
# A = tree_to_list([resource_plr_to_ulab(deck)])
|
# A = tree_to_list([resource_plr_to_ulab(deck)])
|
||||||
# # with open("deck.json", "w", encoding="utf-8") as f:
|
# # with open("deck.json", "w", encoding="utf-8") as f:
|
||||||
# # json.dump(A, f, indent=4, ensure_ascii=False)
|
# # json.dump(A, f, indent=4, ensure_ascii=False)
|
||||||
|
|
||||||
# print(plate11.get_well(0).tracker.get_used_volume())
|
# print(plate11.get_well(0).tracker.get_used_volume())
|
||||||
# Initialize the backend and setup the connection
|
# Initialize the backend and setup the connection
|
||||||
asyncio.run(handler.transfer_group("water", "master_mix", 10)) # Reset tip tracking
|
asyncio.run(handler.transfer_group("water", "master_mix", 10)) # Reset tip tracking
|
||||||
|
|
||||||
|
|
||||||
# asyncio.run(handler.pick_up_tips([plate8.children[8]],[0]))
|
# asyncio.run(handler.pick_up_tips([plate8.children[8]],[0]))
|
||||||
# print(plate8.children[8])
|
# print(plate8.children[8])
|
||||||
# asyncio.run(handler.run_protocol())
|
# asyncio.run(handler.run_protocol())
|
||||||
@@ -1685,121 +1767,118 @@ if __name__ == "__main__":
|
|||||||
# print(plate1.children[0])
|
# print(plate1.children[0])
|
||||||
# asyncio.run(handler.discard_tips([0]))
|
# asyncio.run(handler.discard_tips([0]))
|
||||||
|
|
||||||
# asyncio.run(handler.add_liquid(
|
# asyncio.run(handler.add_liquid(
|
||||||
# asp_vols=[10]*7,
|
# asp_vols=[10]*7,
|
||||||
# dis_vols=[10]*7,
|
# dis_vols=[10]*7,
|
||||||
# reagent_sources=plate11.children[:7],
|
# reagent_sources=plate11.children[:7],
|
||||||
# targets=plate1.children[2:9],
|
# targets=plate1.children[2:9],
|
||||||
# use_channels=[0],
|
# use_channels=[0],
|
||||||
# flow_rates=[None] * 7,
|
# flow_rates=[None] * 7,
|
||||||
# offsets=[Coordinate(0, 0, 0)] * 7,
|
# offsets=[Coordinate(0, 0, 0)] * 7,
|
||||||
# liquid_height=[None] * 7,
|
# liquid_height=[None] * 7,
|
||||||
# blow_out_air_volume=[None] * 2,
|
# blow_out_air_volume=[None] * 2,
|
||||||
# delays=None,
|
# delays=None,
|
||||||
# mix_time=3,
|
# mix_time=3,
|
||||||
# mix_vol=5,
|
# mix_vol=5,
|
||||||
# spread="custom",
|
# spread="custom",
|
||||||
# ))
|
# ))
|
||||||
|
|
||||||
# asyncio.run(handler.run_protocol()) # Run the protocol
|
# asyncio.run(handler.run_protocol()) # Run the protocol
|
||||||
|
|
||||||
|
# # # asyncio.run(handler.transfer_liquid(
|
||||||
|
# # # asp_vols=[10]*2,
|
||||||
|
# # # dis_vols=[10]*2,
|
||||||
|
# # # sources=plate11.children[:2],
|
||||||
|
# # # targets=plate11.children[-2:],
|
||||||
|
# # # use_channels=[0],
|
||||||
|
# # # offsets=[Coordinate(0, 0, 0)] * 4,
|
||||||
|
# # # liquid_height=[None] * 2,
|
||||||
|
# # # blow_out_air_volume=[None] * 2,
|
||||||
|
# # # delays=None,
|
||||||
|
# # # mix_times=3,
|
||||||
|
# # # mix_vol=5,
|
||||||
|
# # # spread="wide",
|
||||||
|
# # # tip_racks=[plate8]
|
||||||
|
# # # ))
|
||||||
|
|
||||||
|
# # # asyncio.run(handler.remove_liquid(
|
||||||
|
# # # vols=[10]*2,
|
||||||
|
# # # sources=plate11.children[:2],
|
||||||
|
# # # waste_liquid=plate11.children[43],
|
||||||
|
# # # use_channels=[0],
|
||||||
|
# # # offsets=[Coordinate(0, 0, 0)] * 4,
|
||||||
|
# # # liquid_height=[None] * 2,
|
||||||
|
# # # blow_out_air_volume=[None] * 2,
|
||||||
|
# # # delays=None,
|
||||||
|
# # # spread="wide"
|
||||||
|
# # # ))
|
||||||
|
# # asyncio.run(handler.run_protocol())
|
||||||
|
|
||||||
|
# # # asyncio.run(handler.discard_tips())
|
||||||
|
# # # asyncio.run(handler.mix(well_containers.children[:8
|
||||||
|
# # # ], mix_time=3, mix_vol=50, height_to_bottom=0.5, offsets=Coordinate(0, 0, 0), mix_rate=100))
|
||||||
|
# # #print(json.dumps(handler._unilabos_backend.steps_todo_list, indent=2)) # Print matrix info
|
||||||
|
|
||||||
# # # asyncio.run(handler.transfer_liquid(
|
# # # asyncio.run(handler.remove_liquid(
|
||||||
# # # asp_vols=[10]*2,
|
# # # vols=[100]*16,
|
||||||
# # # dis_vols=[10]*2,
|
# # # sources=well_containers.children[-16:],
|
||||||
# # # sources=plate11.children[:2],
|
# # # waste_liquid=well_containers.children[:16], # 这个有些奇怪,但是好像也只能这么写
|
||||||
# # # targets=plate11.children[-2:],
|
# # # use_channels=[0, 1, 2, 3, 4, 5, 6, 7],
|
||||||
# # # use_channels=[0],
|
# # # flow_rates=[None] * 32,
|
||||||
# # # offsets=[Coordinate(0, 0, 0)] * 4,
|
# # # offsets=[Coordinate(0, 0, 0)] * 32,
|
||||||
# # # liquid_height=[None] * 2,
|
# # # liquid_height=[None] * 32,
|
||||||
# # # blow_out_air_volume=[None] * 2,
|
# # # blow_out_air_volume=[None] * 32,
|
||||||
# # # delays=None,
|
# # # spread="wide",
|
||||||
# # # mix_times=3,
|
# # # ))
|
||||||
# # # mix_vol=5,
|
# # # asyncio.run(handler.transfer_liquid(
|
||||||
# # # spread="wide",
|
# # # asp_vols=[100]*16,
|
||||||
# # # tip_racks=[plate8]
|
# # # dis_vols=[100]*16,
|
||||||
# # # ))
|
# # # tip_racks=[tip_rack],
|
||||||
|
# # # sources=well_containers.children[-16:],
|
||||||
# # # asyncio.run(handler.remove_liquid(
|
# # # targets=well_containers.children[:16],
|
||||||
# # # vols=[10]*2,
|
# # # use_channels=[0, 1, 2, 3, 4, 5, 6, 7],
|
||||||
# # # sources=plate11.children[:2],
|
# # # offsets=[Coordinate(0, 0, 0)] * 32,
|
||||||
# # # waste_liquid=plate11.children[43],
|
# # # asp_flow_rates=[None] * 16,
|
||||||
# # # use_channels=[0],
|
# # # dis_flow_rates=[None] * 16,
|
||||||
# # # offsets=[Coordinate(0, 0, 0)] * 4,
|
# # # liquid_height=[None] * 32,
|
||||||
# # # liquid_height=[None] * 2,
|
# # # blow_out_air_volume=[None] * 32,
|
||||||
# # # blow_out_air_volume=[None] * 2,
|
# # # mix_times=3,
|
||||||
# # # delays=None,
|
# # # mix_vol=50,
|
||||||
# # # spread="wide"
|
# # # spread="wide",
|
||||||
# # # ))
|
# # # ))
|
||||||
# # asyncio.run(handler.run_protocol())
|
# # print(json.dumps(handler._unilabos_backend.steps_todo_list, indent=2)) # Print matrix info
|
||||||
|
# # # input("pick_up_tips add step")
|
||||||
# # # asyncio.run(handler.discard_tips())
|
# asyncio.run(handler.run_protocol()) # Run the protocol
|
||||||
# # # asyncio.run(handler.mix(well_containers.children[:8
|
# # # input("Running protocol...")
|
||||||
# # # ], mix_time=3, mix_vol=50, height_to_bottom=0.5, offsets=Coordinate(0, 0, 0), mix_rate=100))
|
# # # input("Press Enter to continue...") # Wait for user input before proceeding
|
||||||
# # #print(json.dumps(handler._unilabos_backend.steps_todo_list, indent=2)) # Print matrix info
|
# # # print("PRCXI9300Handler initialized with deck and host settings.")
|
||||||
|
|
||||||
|
|
||||||
# # # asyncio.run(handler.remove_liquid(
|
|
||||||
# # # vols=[100]*16,
|
|
||||||
# # # sources=well_containers.children[-16:],
|
|
||||||
# # # waste_liquid=well_containers.children[:16], # 这个有些奇怪,但是好像也只能这么写
|
|
||||||
# # # use_channels=[0, 1, 2, 3, 4, 5, 6, 7],
|
|
||||||
# # # flow_rates=[None] * 32,
|
|
||||||
# # # offsets=[Coordinate(0, 0, 0)] * 32,
|
|
||||||
# # # liquid_height=[None] * 32,
|
|
||||||
# # # blow_out_air_volume=[None] * 32,
|
|
||||||
# # # spread="wide",
|
|
||||||
# # # ))
|
|
||||||
# # # asyncio.run(handler.transfer_liquid(
|
|
||||||
# # # asp_vols=[100]*16,
|
|
||||||
# # # dis_vols=[100]*16,
|
|
||||||
# # # tip_racks=[tip_rack],
|
|
||||||
# # # sources=well_containers.children[-16:],
|
|
||||||
# # # targets=well_containers.children[:16],
|
|
||||||
# # # use_channels=[0, 1, 2, 3, 4, 5, 6, 7],
|
|
||||||
# # # offsets=[Coordinate(0, 0, 0)] * 32,
|
|
||||||
# # # asp_flow_rates=[None] * 16,
|
|
||||||
# # # dis_flow_rates=[None] * 16,
|
|
||||||
# # # liquid_height=[None] * 32,
|
|
||||||
# # # blow_out_air_volume=[None] * 32,
|
|
||||||
# # # mix_times=3,
|
|
||||||
# # # mix_vol=50,
|
|
||||||
# # # spread="wide",
|
|
||||||
# # # ))
|
|
||||||
# # print(json.dumps(handler._unilabos_backend.steps_todo_list, indent=2)) # Print matrix info
|
|
||||||
# # # input("pick_up_tips add step")
|
|
||||||
#asyncio.run(handler.run_protocol()) # Run the protocol
|
|
||||||
# # # input("Running protocol...")
|
|
||||||
# # # input("Press Enter to continue...") # Wait for user input before proceeding
|
|
||||||
# # # print("PRCXI9300Handler initialized with deck and host settings.")
|
|
||||||
|
|
||||||
|
|
||||||
# 一些推荐版位组合的测试样例:
|
|
||||||
|
|
||||||
# 一些推荐版位组合的测试样例:
|
|
||||||
|
|
||||||
|
# 一些推荐版位组合的测试样例:
|
||||||
|
|
||||||
|
# 一些推荐版位组合的测试样例:
|
||||||
|
|
||||||
with open("prcxi_material.json", "r") as f:
|
with open("prcxi_material.json", "r") as f:
|
||||||
material_info = json.load(f)
|
material_info = json.load(f)
|
||||||
|
|
||||||
layout = DefaultLayout("PRCXI9320")
|
layout = DefaultLayout("PRCXI9320")
|
||||||
layout.add_lab_resource(material_info)
|
layout.add_lab_resource(material_info)
|
||||||
MatrixLayout_1, dict_1 = layout.recommend_layout([
|
MatrixLayout_1, dict_1 = layout.recommend_layout(
|
||||||
("reagent_1", "96 细胞培养皿", 3),
|
[
|
||||||
("reagent_2", "12道储液槽", 1),
|
("reagent_1", "96 细胞培养皿", 3),
|
||||||
("reagent_3", "200μL Tip头", 7),
|
("reagent_2", "12道储液槽", 1),
|
||||||
("reagent_4", "10μL加长 Tip头", 1),
|
("reagent_3", "200μL Tip头", 7),
|
||||||
])
|
("reagent_4", "10μL加长 Tip头", 1),
|
||||||
|
]
|
||||||
|
)
|
||||||
print(dict_1)
|
print(dict_1)
|
||||||
MatrixLayout_2, dict_2 = layout.recommend_layout([
|
MatrixLayout_2, dict_2 = layout.recommend_layout(
|
||||||
("reagent_1", "96深孔板", 4),
|
[
|
||||||
("reagent_2", "12道储液槽", 1),
|
("reagent_1", "96深孔板", 4),
|
||||||
("reagent_3", "200μL Tip头", 1),
|
("reagent_2", "12道储液槽", 1),
|
||||||
("reagent_4", "10μL加长 Tip头", 1),
|
("reagent_3", "200μL Tip头", 1),
|
||||||
])
|
("reagent_4", "10μL加长 Tip头", 1),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# with open("prcxi_material.json", "r") as f:
|
# with open("prcxi_material.json", "r") as f:
|
||||||
# material_info = json.load(f)
|
# material_info = json.load(f)
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import serial.tools.list_ports
|
|||||||
from serial import Serial
|
from serial import Serial
|
||||||
from serial.serialutil import SerialException
|
from serial.serialutil import SerialException
|
||||||
|
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
|
|
||||||
class RunzeSyringePumpMode(Enum):
|
class RunzeSyringePumpMode(Enum):
|
||||||
Normal = 0
|
Normal = 0
|
||||||
@@ -77,6 +79,8 @@ class RunzeSyringePumpInfo:
|
|||||||
|
|
||||||
|
|
||||||
class RunzeSyringePumpAsync:
|
class RunzeSyringePumpAsync:
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, port: str, address: str = "1", volume: float = 25000, mode: RunzeSyringePumpMode = None):
|
def __init__(self, port: str, address: str = "1", volume: float = 25000, mode: RunzeSyringePumpMode = None):
|
||||||
self.port = port
|
self.port = port
|
||||||
self.address = address
|
self.address = address
|
||||||
@@ -102,6 +106,9 @@ class RunzeSyringePumpAsync:
|
|||||||
self._run_future: Optional[Future[Any]] = None
|
self._run_future: Optional[Future[Any]] = None
|
||||||
self._run_lock = Lock()
|
self._run_lock = Lock()
|
||||||
|
|
||||||
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
|
self._ros_node = ros_node
|
||||||
|
|
||||||
def _adjust_total_steps(self):
|
def _adjust_total_steps(self):
|
||||||
self.total_steps = 6000 if self.mode == RunzeSyringePumpMode.Normal else 48000
|
self.total_steps = 6000 if self.mode == RunzeSyringePumpMode.Normal else 48000
|
||||||
self.total_steps_vel = 48000 if self.mode == RunzeSyringePumpMode.AccuratePosVel else 6000
|
self.total_steps_vel = 48000 if self.mode == RunzeSyringePumpMode.AccuratePosVel else 6000
|
||||||
@@ -182,7 +189,7 @@ class RunzeSyringePumpAsync:
|
|||||||
try:
|
try:
|
||||||
await self._query(command)
|
await self._query(command)
|
||||||
while True:
|
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()
|
status = await self.query_device_status()
|
||||||
if status == '`':
|
if status == '`':
|
||||||
@@ -364,7 +371,7 @@ class RunzeSyringePumpAsync:
|
|||||||
if self._read_task:
|
if self._read_task:
|
||||||
raise RunzeSyringePumpConnectionError
|
raise RunzeSyringePumpConnectionError
|
||||||
|
|
||||||
self._read_task = asyncio.create_task(self._read_loop())
|
self._read_task = self._ros_node.create_task(self._read_loop())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.query_device_status()
|
await self.query_device_status()
|
||||||
|
|||||||
@@ -3,10 +3,14 @@ import logging
|
|||||||
import time as time_module
|
import time as time_module
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
|
|
||||||
class VirtualCentrifuge:
|
class VirtualCentrifuge:
|
||||||
"""Virtual centrifuge device - 简化版,只保留核心功能"""
|
"""Virtual centrifuge device - 简化版,只保留核心功能"""
|
||||||
|
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
||||||
# 处理可能的不同调用方式
|
# 处理可能的不同调用方式
|
||||||
if device_id is None and "id" in kwargs:
|
if device_id is None and "id" in kwargs:
|
||||||
@@ -33,6 +37,9 @@ class VirtualCentrifuge:
|
|||||||
if key not in skip_keys and not hasattr(self, key):
|
if key not in skip_keys and not hasattr(self, key):
|
||||||
setattr(self, key, value)
|
setattr(self, key, value)
|
||||||
|
|
||||||
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
|
self._ros_node = ros_node
|
||||||
|
|
||||||
async def initialize(self) -> bool:
|
async def initialize(self) -> bool:
|
||||||
"""Initialize virtual centrifuge"""
|
"""Initialize virtual centrifuge"""
|
||||||
self.logger.info(f"Initializing virtual centrifuge {self.device_id}")
|
self.logger.info(f"Initializing virtual centrifuge {self.device_id}")
|
||||||
@@ -132,7 +139,7 @@ class VirtualCentrifuge:
|
|||||||
break
|
break
|
||||||
|
|
||||||
# 每秒更新一次
|
# 每秒更新一次
|
||||||
await asyncio.sleep(1.0)
|
await self._ros_node.sleep(1.0)
|
||||||
|
|
||||||
# 离心完成
|
# 离心完成
|
||||||
self.data.update({
|
self.data.update({
|
||||||
|
|||||||
@@ -2,9 +2,13 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
class VirtualColumn:
|
class VirtualColumn:
|
||||||
"""Virtual column device for RunColumn protocol 🏛️"""
|
"""Virtual column device for RunColumn protocol 🏛️"""
|
||||||
|
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
|
def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
|
||||||
# 处理可能的不同调用方式
|
# 处理可能的不同调用方式
|
||||||
if device_id is None and 'id' in kwargs:
|
if device_id is None and 'id' in kwargs:
|
||||||
@@ -28,6 +32,9 @@ class VirtualColumn:
|
|||||||
print(f"🏛️ === 虚拟色谱柱 {self.device_id} 已创建 === ✨")
|
print(f"🏛️ === 虚拟色谱柱 {self.device_id} 已创建 === ✨")
|
||||||
print(f"📏 柱参数: 流速={self._max_flow_rate}mL/min | 长度={self._column_length}cm | 直径={self._column_diameter}cm 🔬")
|
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:
|
async def initialize(self) -> bool:
|
||||||
"""Initialize virtual column 🚀"""
|
"""Initialize virtual column 🚀"""
|
||||||
self.logger.info(f"🔧 初始化虚拟色谱柱 {self.device_id} ✨")
|
self.logger.info(f"🔧 初始化虚拟色谱柱 {self.device_id} ✨")
|
||||||
@@ -101,7 +108,7 @@ class VirtualColumn:
|
|||||||
step_time = separation_time / steps
|
step_time = separation_time / steps
|
||||||
|
|
||||||
for i in range(steps):
|
for i in range(steps):
|
||||||
await asyncio.sleep(step_time)
|
await self._ros_node.sleep(step_time)
|
||||||
|
|
||||||
progress = (i + 1) / steps * 100
|
progress = (i + 1) / steps * 100
|
||||||
volume_processed = (i + 1) * 5.0 # 假设每步处理5mL
|
volume_processed = (i + 1) * 5.0 # 假设每步处理5mL
|
||||||
|
|||||||
@@ -4,16 +4,19 @@ import time as time_module
|
|||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
from unilabos.compile.utils.vessel_parser import get_vessel
|
from unilabos.compile.utils.vessel_parser import get_vessel
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
|
|
||||||
class VirtualFilter:
|
class VirtualFilter:
|
||||||
"""Virtual filter device - 完全按照 Filter.action 规范 🌊"""
|
"""Virtual filter device - 完全按照 Filter.action 规范 🌊"""
|
||||||
|
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
||||||
if device_id is None and 'id' in kwargs:
|
if device_id is None and "id" in kwargs:
|
||||||
device_id = kwargs.pop('id')
|
device_id = kwargs.pop("id")
|
||||||
if config is None and 'config' in kwargs:
|
if config is None and "config" in kwargs:
|
||||||
config = kwargs.pop('config')
|
config = kwargs.pop("config")
|
||||||
|
|
||||||
self.device_id = device_id or "unknown_filter"
|
self.device_id = device_id or "unknown_filter"
|
||||||
self.config = config or {}
|
self.config = config or {}
|
||||||
@@ -21,29 +24,34 @@ class VirtualFilter:
|
|||||||
self.data = {}
|
self.data = {}
|
||||||
|
|
||||||
# 从config或kwargs中获取配置参数
|
# 从config或kwargs中获取配置参数
|
||||||
self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL')
|
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_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_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._max_volume = self.config.get("max_volume") or kwargs.get("max_volume", 500.0)
|
||||||
|
|
||||||
# 处理其他kwargs参数
|
# 处理其他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():
|
for key, value in kwargs.items():
|
||||||
if key not in skip_keys and not hasattr(self, key):
|
if key not in skip_keys and not hasattr(self, key):
|
||||||
setattr(self, key, value)
|
setattr(self, key, value)
|
||||||
|
|
||||||
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
|
self._ros_node = ros_node
|
||||||
|
|
||||||
async def initialize(self) -> bool:
|
async def initialize(self) -> bool:
|
||||||
"""Initialize virtual filter 🚀"""
|
"""Initialize virtual filter 🚀"""
|
||||||
self.logger.info(f"🔧 初始化虚拟过滤器 {self.device_id} ✨")
|
self.logger.info(f"🔧 初始化虚拟过滤器 {self.device_id} ✨")
|
||||||
|
|
||||||
# 按照 Filter.action 的 feedback 字段初始化
|
# 按照 Filter.action 的 feedback 字段初始化
|
||||||
self.data.update({
|
self.data.update(
|
||||||
"status": "Idle",
|
{
|
||||||
"progress": 0.0, # Filter.action feedback
|
"status": "Idle",
|
||||||
"current_temp": 25.0, # Filter.action feedback
|
"progress": 0.0, # Filter.action feedback
|
||||||
"filtered_volume": 0.0, # Filter.action feedback
|
"current_temp": 25.0, # Filter.action feedback
|
||||||
"message": "Ready for filtration"
|
"filtered_volume": 0.0, # Filter.action feedback
|
||||||
})
|
"message": "Ready for filtration",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
self.logger.info(f"✅ 过滤器 {self.device_id} 初始化完成 🌊")
|
self.logger.info(f"✅ 过滤器 {self.device_id} 初始化完成 🌊")
|
||||||
return True
|
return True
|
||||||
@@ -52,9 +60,7 @@ class VirtualFilter:
|
|||||||
"""Cleanup virtual filter 🧹"""
|
"""Cleanup virtual filter 🧹"""
|
||||||
self.logger.info(f"🧹 清理虚拟过滤器 {self.device_id} 🔚")
|
self.logger.info(f"🧹 清理虚拟过滤器 {self.device_id} 🔚")
|
||||||
|
|
||||||
self.data.update({
|
self.data.update({"status": "Offline"})
|
||||||
"status": "Offline"
|
|
||||||
})
|
|
||||||
|
|
||||||
self.logger.info(f"✅ 过滤器 {self.device_id} 清理完成 💤")
|
self.logger.info(f"✅ 过滤器 {self.device_id} 清理完成 💤")
|
||||||
return True
|
return True
|
||||||
@@ -67,7 +73,7 @@ class VirtualFilter:
|
|||||||
stir_speed: float = 300.0,
|
stir_speed: float = 300.0,
|
||||||
temp: float = 25.0,
|
temp: float = 25.0,
|
||||||
continue_heatchill: bool = False,
|
continue_heatchill: bool = False,
|
||||||
volume: float = 0.0
|
volume: float = 0.0,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Execute filter action - 完全按照 Filter.action 参数 🌊"""
|
"""Execute filter action - 完全按照 Filter.action 参数 🌊"""
|
||||||
vessel_id, _ = get_vessel(vessel)
|
vessel_id, _ = get_vessel(vessel)
|
||||||
@@ -79,7 +85,7 @@ class VirtualFilter:
|
|||||||
temp = 25.0 # 0度自动设置为室温
|
temp = 25.0 # 0度自动设置为室温
|
||||||
self.logger.info(f"🌡️ 温度自动调整: {original_temp}°C → {temp}°C (室温) 🏠")
|
self.logger.info(f"🌡️ 温度自动调整: {original_temp}°C → {temp}°C (室温) 🏠")
|
||||||
elif temp < 4.0:
|
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"🌡️ 温度自动调整: {original_temp}°C → {temp}°C (最低温度) ❄️")
|
||||||
|
|
||||||
self.logger.info(f"🌊 开始过滤操作: {vessel_id} → {filtrate_vessel_id} 🚰")
|
self.logger.info(f"🌊 开始过滤操作: {vessel_id} → {filtrate_vessel_id} 🚰")
|
||||||
@@ -92,41 +98,34 @@ class VirtualFilter:
|
|||||||
if temp > self._max_temp or temp < 4.0:
|
if temp > self._max_temp or temp < 4.0:
|
||||||
error_msg = f"🌡️ 温度 {temp}°C 超出范围 (4-{self._max_temp}°C) ⚠️"
|
error_msg = f"🌡️ 温度 {temp}°C 超出范围 (4-{self._max_temp}°C) ⚠️"
|
||||||
self.logger.error(f"❌ {error_msg}")
|
self.logger.error(f"❌ {error_msg}")
|
||||||
self.data.update({
|
self.data.update({"status": f"Error: 温度超出范围 ⚠️", "message": error_msg})
|
||||||
"status": f"Error: 温度超出范围 ⚠️",
|
|
||||||
"message": error_msg
|
|
||||||
})
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if stir and stir_speed > self._max_stir_speed:
|
if stir and stir_speed > self._max_stir_speed:
|
||||||
error_msg = f"🌪️ 搅拌速度 {stir_speed} RPM 超出范围 (0-{self._max_stir_speed} RPM) ⚠️"
|
error_msg = f"🌪️ 搅拌速度 {stir_speed} RPM 超出范围 (0-{self._max_stir_speed} RPM) ⚠️"
|
||||||
self.logger.error(f"❌ {error_msg}")
|
self.logger.error(f"❌ {error_msg}")
|
||||||
self.data.update({
|
self.data.update({"status": f"Error: 搅拌速度超出范围 ⚠️", "message": error_msg})
|
||||||
"status": f"Error: 搅拌速度超出范围 ⚠️",
|
|
||||||
"message": error_msg
|
|
||||||
})
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if volume > self._max_volume:
|
if volume > self._max_volume:
|
||||||
error_msg = f"💧 过滤体积 {volume} mL 超出范围 (0-{self._max_volume} mL) ⚠️"
|
error_msg = f"💧 过滤体积 {volume} mL 超出范围 (0-{self._max_volume} mL) ⚠️"
|
||||||
self.logger.error(f"❌ {error_msg}")
|
self.logger.error(f"❌ {error_msg}")
|
||||||
self.data.update({
|
self.data.update({"status": f"Error", "message": error_msg})
|
||||||
"status": f"Error",
|
|
||||||
"message": error_msg
|
|
||||||
})
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 开始过滤
|
# 开始过滤
|
||||||
filter_volume = volume if volume > 0 else 50.0
|
filter_volume = volume if volume > 0 else 50.0
|
||||||
self.logger.info(f"🚀 开始过滤 {filter_volume}mL 液体 💧")
|
self.logger.info(f"🚀 开始过滤 {filter_volume}mL 液体 💧")
|
||||||
|
|
||||||
self.data.update({
|
self.data.update(
|
||||||
"status": f"Running",
|
{
|
||||||
"current_temp": temp,
|
"status": f"Running",
|
||||||
"filtered_volume": 0.0,
|
"current_temp": temp,
|
||||||
"progress": 0.0,
|
"filtered_volume": 0.0,
|
||||||
"message": f"🚀 Starting filtration: {vessel_id} → {filtrate_vessel_id}"
|
"progress": 0.0,
|
||||||
})
|
"message": f"🚀 Starting filtration: {vessel_id} → {filtrate_vessel_id}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 过滤过程 - 实时更新进度
|
# 过滤过程 - 实时更新进度
|
||||||
@@ -157,13 +156,15 @@ class VirtualFilter:
|
|||||||
status_msg += f" | 🌪️ 搅拌: {stir_speed} RPM"
|
status_msg += f" | 🌪️ 搅拌: {stir_speed} RPM"
|
||||||
status_msg += f" | 🌡️ {temp}°C | 📊 {progress:.1f}% | 💧 已过滤: {current_filtered:.1f}mL"
|
status_msg += f" | 🌡️ {temp}°C | 📊 {progress:.1f}% | 💧 已过滤: {current_filtered:.1f}mL"
|
||||||
|
|
||||||
self.data.update({
|
self.data.update(
|
||||||
"progress": progress, # Filter.action feedback
|
{
|
||||||
"current_temp": temp, # Filter.action feedback
|
"progress": progress, # Filter.action feedback
|
||||||
"filtered_volume": current_filtered, # Filter.action feedback
|
"current_temp": temp, # Filter.action feedback
|
||||||
"status": "Running",
|
"filtered_volume": current_filtered, # Filter.action feedback
|
||||||
"message": f"🌊 Filtering: {progress:.1f}% complete, {current_filtered:.1f}mL filtered"
|
"status": "Running",
|
||||||
})
|
"message": f"🌊 Filtering: {progress:.1f}% complete, {current_filtered:.1f}mL filtered",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# 进度日志(每25%打印一次)
|
# 进度日志(每25%打印一次)
|
||||||
if progress >= 25 and progress % 25 < 1:
|
if progress >= 25 and progress % 25 < 1:
|
||||||
@@ -172,7 +173,7 @@ class VirtualFilter:
|
|||||||
if remaining <= 0:
|
if remaining <= 0:
|
||||||
break
|
break
|
||||||
|
|
||||||
await asyncio.sleep(1.0)
|
await self._ros_node.sleep(1.0)
|
||||||
|
|
||||||
# 过滤完成
|
# 过滤完成
|
||||||
final_temp = temp if continue_heatchill else 25.0
|
final_temp = temp if continue_heatchill else 25.0
|
||||||
@@ -181,13 +182,15 @@ class VirtualFilter:
|
|||||||
final_status += " | 🔥 继续加热搅拌"
|
final_status += " | 🔥 继续加热搅拌"
|
||||||
self.logger.info(f"🔥 继续保持加热搅拌状态 🌪️")
|
self.logger.info(f"🔥 继续保持加热搅拌状态 🌪️")
|
||||||
|
|
||||||
self.data.update({
|
self.data.update(
|
||||||
"status": final_status,
|
{
|
||||||
"progress": 100.0, # Filter.action feedback
|
"status": final_status,
|
||||||
"current_temp": final_temp, # Filter.action feedback
|
"progress": 100.0, # Filter.action feedback
|
||||||
"filtered_volume": filter_volume, # Filter.action feedback
|
"current_temp": final_temp, # Filter.action feedback
|
||||||
"message": f"✅ Filtration completed: {filter_volume}mL filtered from {vessel_id}"
|
"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"🎉 过滤完成! 💧 {filter_volume}mL 从 {vessel_id} 过滤到 {filtrate_vessel_id} ✨")
|
||||||
self.logger.info(f"📊 最终状态: 温度 {final_temp}°C | 进度 100% | 体积 {filter_volume}mL 🏁")
|
self.logger.info(f"📊 最终状态: 温度 {final_temp}°C | 进度 100% | 体积 {filter_volume}mL 🏁")
|
||||||
@@ -196,10 +199,7 @@ class VirtualFilter:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"过滤过程中发生错误: {str(e)} 💥"
|
error_msg = f"过滤过程中发生错误: {str(e)} 💥"
|
||||||
self.logger.error(f"❌ {error_msg}")
|
self.logger.error(f"❌ {error_msg}")
|
||||||
self.data.update({
|
self.data.update({"status": f"Error", "message": f"❌ Filtration failed: {str(e)}"})
|
||||||
"status": f"Error",
|
|
||||||
"message": f"❌ Filtration failed: {str(e)}"
|
|
||||||
})
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# === 核心状态属性 - 按照 Filter.action feedback 字段 ===
|
# === 核心状态属性 - 按照 Filter.action feedback 字段 ===
|
||||||
|
|||||||
@@ -3,9 +3,13 @@ import logging
|
|||||||
import time as time_module # 重命名time模块,避免与参数冲突
|
import time as time_module # 重命名time模块,避免与参数冲突
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
class VirtualHeatChill:
|
class VirtualHeatChill:
|
||||||
"""Virtual heat chill device for HeatChillProtocol testing 🌡️"""
|
"""Virtual heat chill device for HeatChillProtocol testing 🌡️"""
|
||||||
|
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
|
def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
|
||||||
# 处理可能的不同调用方式
|
# 处理可能的不同调用方式
|
||||||
if device_id is None and 'id' in kwargs:
|
if device_id is None and 'id' in kwargs:
|
||||||
@@ -35,6 +39,9 @@ class VirtualHeatChill:
|
|||||||
print(f"🌡️ === 虚拟温控设备 {self.device_id} 已创建 === ✨")
|
print(f"🌡️ === 虚拟温控设备 {self.device_id} 已创建 === ✨")
|
||||||
print(f"🔥 温度范围: {self._min_temp}°C ~ {self._max_temp}°C | 🌪️ 最大搅拌: {self._max_stir_speed} RPM")
|
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:
|
async def initialize(self) -> bool:
|
||||||
"""Initialize virtual heat chill 🚀"""
|
"""Initialize virtual heat chill 🚀"""
|
||||||
self.logger.info(f"🔧 初始化虚拟温控设备 {self.device_id} ✨")
|
self.logger.info(f"🔧 初始化虚拟温控设备 {self.device_id} ✨")
|
||||||
@@ -177,7 +184,7 @@ class VirtualHeatChill:
|
|||||||
break
|
break
|
||||||
|
|
||||||
# 等待1秒后再次检查
|
# 等待1秒后再次检查
|
||||||
await asyncio.sleep(1.0)
|
await self._ros_node.sleep(1.0)
|
||||||
|
|
||||||
# 操作完成
|
# 操作完成
|
||||||
final_stir_info = f" | 🌪️ 搅拌: {stir_speed} RPM" if stir else ""
|
final_stir_info = f" | 🌪️ 搅拌: {stir_speed} RPM" if stir else ""
|
||||||
|
|||||||
@@ -3,13 +3,19 @@ import logging
|
|||||||
import time as time_module
|
import time as time_module
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
|
|
||||||
def debug_print(message):
|
def debug_print(message):
|
||||||
"""调试输出 🔍"""
|
"""调试输出 🔍"""
|
||||||
print(f"🌪️ [ROTAVAP] {message}", flush=True)
|
print(f"🌪️ [ROTAVAP] {message}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
class VirtualRotavap:
|
class VirtualRotavap:
|
||||||
"""Virtual rotary evaporator device - 简化版,只保留核心功能 🌪️"""
|
"""Virtual rotary evaporator device - 简化版,只保留核心功能 🌪️"""
|
||||||
|
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
||||||
# 处理可能的不同调用方式
|
# 处理可能的不同调用方式
|
||||||
if device_id is None and "id" in kwargs:
|
if device_id is None and "id" in kwargs:
|
||||||
@@ -38,40 +44,49 @@ class VirtualRotavap:
|
|||||||
print(f"🌪️ === 虚拟旋转蒸发仪 {self.device_id} 已创建 === ✨")
|
print(f"🌪️ === 虚拟旋转蒸发仪 {self.device_id} 已创建 === ✨")
|
||||||
print(f"🔥 温度范围: 10°C ~ {self._max_temp}°C | 🌀 转速范围: 10 ~ {self._max_rotation_speed} RPM")
|
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:
|
async def initialize(self) -> bool:
|
||||||
"""Initialize virtual rotary evaporator 🚀"""
|
"""Initialize virtual rotary evaporator 🚀"""
|
||||||
self.logger.info(f"🔧 初始化虚拟旋转蒸发仪 {self.device_id} ✨")
|
self.logger.info(f"🔧 初始化虚拟旋转蒸发仪 {self.device_id} ✨")
|
||||||
|
|
||||||
# 只保留核心状态
|
# 只保留核心状态
|
||||||
self.data.update({
|
self.data.update(
|
||||||
"status": "🏠 待机中",
|
{
|
||||||
"rotavap_state": "Ready", # Ready, Evaporating, Completed, Error
|
"status": "🏠 待机中",
|
||||||
"current_temp": 25.0,
|
"rotavap_state": "Ready", # Ready, Evaporating, Completed, Error
|
||||||
"target_temp": 25.0,
|
"current_temp": 25.0,
|
||||||
"rotation_speed": 0.0,
|
"target_temp": 25.0,
|
||||||
"vacuum_pressure": 1.0, # 大气压
|
"rotation_speed": 0.0,
|
||||||
"evaporated_volume": 0.0,
|
"vacuum_pressure": 1.0, # 大气压
|
||||||
"progress": 0.0,
|
"evaporated_volume": 0.0,
|
||||||
"remaining_time": 0.0,
|
"progress": 0.0,
|
||||||
"message": "🌪️ Ready for evaporation"
|
"remaining_time": 0.0,
|
||||||
})
|
"message": "🌪️ Ready for evaporation",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
self.logger.info(f"✅ 旋转蒸发仪 {self.device_id} 初始化完成 🌪️")
|
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
|
return True
|
||||||
|
|
||||||
async def cleanup(self) -> bool:
|
async def cleanup(self) -> bool:
|
||||||
"""Cleanup virtual rotary evaporator 🧹"""
|
"""Cleanup virtual rotary evaporator 🧹"""
|
||||||
self.logger.info(f"🧹 清理虚拟旋转蒸发仪 {self.device_id} 🔚")
|
self.logger.info(f"🧹 清理虚拟旋转蒸发仪 {self.device_id} 🔚")
|
||||||
|
|
||||||
self.data.update({
|
self.data.update(
|
||||||
"status": "💤 离线",
|
{
|
||||||
"rotavap_state": "Offline",
|
"status": "💤 离线",
|
||||||
"current_temp": 25.0,
|
"rotavap_state": "Offline",
|
||||||
"rotation_speed": 0.0,
|
"current_temp": 25.0,
|
||||||
"vacuum_pressure": 1.0,
|
"rotation_speed": 0.0,
|
||||||
"message": "💤 System offline"
|
"vacuum_pressure": 1.0,
|
||||||
})
|
"message": "💤 System offline",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
self.logger.info(f"✅ 旋转蒸发仪 {self.device_id} 清理完成 💤")
|
self.logger.info(f"✅ 旋转蒸发仪 {self.device_id} 清理完成 💤")
|
||||||
return True
|
return True
|
||||||
@@ -84,7 +99,7 @@ class VirtualRotavap:
|
|||||||
time: float = 180.0,
|
time: float = 180.0,
|
||||||
stir_speed: float = 100.0,
|
stir_speed: float = 100.0,
|
||||||
solvent: str = "",
|
solvent: str = "",
|
||||||
**kwargs
|
**kwargs,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Execute evaporate action - 简化版 🌪️"""
|
"""Execute evaporate action - 简化版 🌪️"""
|
||||||
|
|
||||||
@@ -114,11 +129,11 @@ class VirtualRotavap:
|
|||||||
self.logger.info(f"🧪 识别到溶剂: {solvent}")
|
self.logger.info(f"🧪 识别到溶剂: {solvent}")
|
||||||
# 根据溶剂调整参数
|
# 根据溶剂调整参数
|
||||||
solvent_lower = solvent.lower()
|
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)
|
temp = max(temp, 80.0)
|
||||||
pressure = max(pressure, 0.2)
|
pressure = max(pressure, 0.2)
|
||||||
self.logger.info(f"💧 水系溶剂:调整参数 → 温度 {temp}°C, 压力 {pressure} bar")
|
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)
|
temp = min(temp, 50.0)
|
||||||
pressure = min(pressure, 0.05)
|
pressure = min(pressure, 0.05)
|
||||||
self.logger.info(f"⚡ 易挥发溶剂:调整参数 → 温度 {temp}°C, 压力 {pressure} bar")
|
self.logger.info(f"⚡ 易挥发溶剂:调整参数 → 温度 {temp}°C, 压力 {pressure} bar")
|
||||||
@@ -136,57 +151,65 @@ class VirtualRotavap:
|
|||||||
if temp > self._max_temp or temp < 10.0:
|
if temp > self._max_temp or temp < 10.0:
|
||||||
error_msg = f"🌡️ 温度 {temp}°C 超出范围 (10-{self._max_temp}°C) ⚠️"
|
error_msg = f"🌡️ 温度 {temp}°C 超出范围 (10-{self._max_temp}°C) ⚠️"
|
||||||
self.logger.error(f"❌ {error_msg}")
|
self.logger.error(f"❌ {error_msg}")
|
||||||
self.data.update({
|
self.data.update(
|
||||||
"status": f"❌ 错误: 温度超出范围",
|
{
|
||||||
"rotavap_state": "Error",
|
"status": f"❌ 错误: 温度超出范围",
|
||||||
"current_temp": 25.0,
|
"rotavap_state": "Error",
|
||||||
"progress": 0.0,
|
"current_temp": 25.0,
|
||||||
"evaporated_volume": 0.0,
|
"progress": 0.0,
|
||||||
"message": error_msg
|
"evaporated_volume": 0.0,
|
||||||
})
|
"message": error_msg,
|
||||||
|
}
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if stir_speed > self._max_rotation_speed or stir_speed < 10.0:
|
if stir_speed > self._max_rotation_speed or stir_speed < 10.0:
|
||||||
error_msg = f"🌀 旋转速度 {stir_speed} RPM 超出范围 (10-{self._max_rotation_speed} RPM) ⚠️"
|
error_msg = f"🌀 旋转速度 {stir_speed} RPM 超出范围 (10-{self._max_rotation_speed} RPM) ⚠️"
|
||||||
self.logger.error(f"❌ {error_msg}")
|
self.logger.error(f"❌ {error_msg}")
|
||||||
self.data.update({
|
self.data.update(
|
||||||
"status": f"❌ 错误: 转速超出范围",
|
{
|
||||||
"rotavap_state": "Error",
|
"status": f"❌ 错误: 转速超出范围",
|
||||||
"current_temp": 25.0,
|
"rotavap_state": "Error",
|
||||||
"progress": 0.0,
|
"current_temp": 25.0,
|
||||||
"evaporated_volume": 0.0,
|
"progress": 0.0,
|
||||||
"message": error_msg
|
"evaporated_volume": 0.0,
|
||||||
})
|
"message": error_msg,
|
||||||
|
}
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if pressure < 0.01 or pressure > 1.0:
|
if pressure < 0.01 or pressure > 1.0:
|
||||||
error_msg = f"💨 真空度 {pressure} bar 超出范围 (0.01-1.0 bar) ⚠️"
|
error_msg = f"💨 真空度 {pressure} bar 超出范围 (0.01-1.0 bar) ⚠️"
|
||||||
self.logger.error(f"❌ {error_msg}")
|
self.logger.error(f"❌ {error_msg}")
|
||||||
self.data.update({
|
self.data.update(
|
||||||
"status": f"❌ 错误: 压力超出范围",
|
{
|
||||||
"rotavap_state": "Error",
|
"status": f"❌ 错误: 压力超出范围",
|
||||||
"current_temp": 25.0,
|
"rotavap_state": "Error",
|
||||||
"progress": 0.0,
|
"current_temp": 25.0,
|
||||||
"evaporated_volume": 0.0,
|
"progress": 0.0,
|
||||||
"message": error_msg
|
"evaporated_volume": 0.0,
|
||||||
})
|
"message": error_msg,
|
||||||
|
}
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 开始蒸发 - 🔧 现在time已经确保是float类型
|
# 开始蒸发 - 🔧 现在time已经确保是float类型
|
||||||
self.logger.info(f"🚀 启动蒸发程序! 预计用时 {time/60:.1f}分钟 ⏱️")
|
self.logger.info(f"🚀 启动蒸发程序! 预计用时 {time/60:.1f}分钟 ⏱️")
|
||||||
|
|
||||||
self.data.update({
|
self.data.update(
|
||||||
"status": f"🌪️ 蒸发中: {actual_vessel}",
|
{
|
||||||
"rotavap_state": "Evaporating",
|
"status": f"🌪️ 蒸发中: {actual_vessel}",
|
||||||
"current_temp": temp,
|
"rotavap_state": "Evaporating",
|
||||||
"target_temp": temp,
|
"current_temp": temp,
|
||||||
"rotation_speed": stir_speed,
|
"target_temp": temp,
|
||||||
"vacuum_pressure": pressure,
|
"rotation_speed": stir_speed,
|
||||||
"remaining_time": time,
|
"vacuum_pressure": pressure,
|
||||||
"progress": 0.0,
|
"remaining_time": time,
|
||||||
"evaporated_volume": 0.0,
|
"progress": 0.0,
|
||||||
"message": f"🌪️ Evaporating {actual_vessel} at {temp}°C, {pressure} bar, {stir_speed} RPM"
|
"evaporated_volume": 0.0,
|
||||||
})
|
"message": f"🌪️ Evaporating {actual_vessel} at {temp}°C, {pressure} bar, {stir_speed} RPM",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 蒸发过程 - 实时更新进度
|
# 蒸发过程 - 实时更新进度
|
||||||
@@ -201,9 +224,9 @@ class VirtualRotavap:
|
|||||||
progress = min(100.0, (elapsed / total_time) * 100)
|
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 # 水系溶剂蒸发慢
|
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 # 易挥发溶剂蒸发快
|
evaporated_vol = progress * 1.0 # 易挥发溶剂蒸发快
|
||||||
else:
|
else:
|
||||||
evaporated_vol = progress * 0.8 # 默认蒸发量
|
evaporated_vol = progress * 0.8 # 默认蒸发量
|
||||||
@@ -211,18 +234,22 @@ class VirtualRotavap:
|
|||||||
# 🔧 更新状态 - 确保包含所有必需字段
|
# 🔧 更新状态 - 确保包含所有必需字段
|
||||||
status_msg = f"🌪️ 蒸发中: {actual_vessel} | 🌡️ {temp}°C | 💨 {pressure} bar | 🌀 {stir_speed} RPM | 📊 {progress:.1f}% | ⏰ 剩余: {remaining:.0f}s"
|
status_msg = f"🌪️ 蒸发中: {actual_vessel} | 🌡️ {temp}°C | 💨 {pressure} bar | 🌀 {stir_speed} RPM | 📊 {progress:.1f}% | ⏰ 剩余: {remaining:.0f}s"
|
||||||
|
|
||||||
self.data.update({
|
self.data.update(
|
||||||
"remaining_time": remaining,
|
{
|
||||||
"progress": progress,
|
"remaining_time": remaining,
|
||||||
"evaporated_volume": evaporated_vol,
|
"progress": progress,
|
||||||
"current_temp": temp,
|
"evaporated_volume": evaporated_vol,
|
||||||
"status": status_msg,
|
"current_temp": temp,
|
||||||
"message": f"🌪️ Evaporating: {progress:.1f}% complete, 💧 {evaporated_vol:.1f}mL evaporated, ⏰ {remaining:.0f}s remaining"
|
"status": status_msg,
|
||||||
})
|
"message": f"🌪️ Evaporating: {progress:.1f}% complete, 💧 {evaporated_vol:.1f}mL evaporated, ⏰ {remaining:.0f}s remaining",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# 进度日志(每25%打印一次)
|
# 进度日志(每25%打印一次)
|
||||||
if progress >= 25 and int(progress) % 25 == 0 and int(progress) != last_logged_progress:
|
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)
|
last_logged_progress = int(progress)
|
||||||
|
|
||||||
# 时间到了,退出循环
|
# 时间到了,退出循环
|
||||||
@@ -230,27 +257,29 @@ class VirtualRotavap:
|
|||||||
break
|
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 # 水系溶剂
|
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 # 易挥发溶剂
|
final_evaporated = 100.0 # 易挥发溶剂
|
||||||
else:
|
else:
|
||||||
final_evaporated = 80.0 # 默认
|
final_evaporated = 80.0 # 默认
|
||||||
|
|
||||||
self.data.update({
|
self.data.update(
|
||||||
"status": f"✅ 蒸发完成: {actual_vessel} | 💧 蒸发量: {final_evaporated:.1f}mL",
|
{
|
||||||
"rotavap_state": "Completed",
|
"status": f"✅ 蒸发完成: {actual_vessel} | 💧 蒸发量: {final_evaporated:.1f}mL",
|
||||||
"evaporated_volume": final_evaporated,
|
"rotavap_state": "Completed",
|
||||||
"progress": 100.0,
|
"evaporated_volume": final_evaporated,
|
||||||
"current_temp": temp,
|
"progress": 100.0,
|
||||||
"remaining_time": 0.0,
|
"current_temp": temp,
|
||||||
"rotation_speed": 0.0,
|
"remaining_time": 0.0,
|
||||||
"vacuum_pressure": 1.0,
|
"rotation_speed": 0.0,
|
||||||
"message": f"✅ Evaporation completed: {final_evaporated}mL evaporated from {actual_vessel}"
|
"vacuum_pressure": 1.0,
|
||||||
})
|
"message": f"✅ Evaporation completed: {final_evaporated}mL evaporated from {actual_vessel}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
self.logger.info(f"🎉 蒸发操作完成! ✨")
|
self.logger.info(f"🎉 蒸发操作完成! ✨")
|
||||||
self.logger.info(f"📊 蒸发结果:")
|
self.logger.info(f"📊 蒸发结果:")
|
||||||
@@ -270,16 +299,18 @@ class VirtualRotavap:
|
|||||||
error_msg = f"蒸发过程中发生错误: {str(e)} 💥"
|
error_msg = f"蒸发过程中发生错误: {str(e)} 💥"
|
||||||
self.logger.error(f"❌ {error_msg}")
|
self.logger.error(f"❌ {error_msg}")
|
||||||
|
|
||||||
self.data.update({
|
self.data.update(
|
||||||
"status": f"❌ 蒸发错误: {str(e)}",
|
{
|
||||||
"rotavap_state": "Error",
|
"status": f"❌ 蒸发错误: {str(e)}",
|
||||||
"current_temp": 25.0,
|
"rotavap_state": "Error",
|
||||||
"progress": 0.0,
|
"current_temp": 25.0,
|
||||||
"evaporated_volume": 0.0,
|
"progress": 0.0,
|
||||||
"rotation_speed": 0.0,
|
"evaporated_volume": 0.0,
|
||||||
"vacuum_pressure": 1.0,
|
"rotation_speed": 0.0,
|
||||||
"message": f"❌ Evaporation failed: {str(e)}"
|
"vacuum_pressure": 1.0,
|
||||||
})
|
"message": f"❌ Evaporation failed: {str(e)}",
|
||||||
|
}
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# === 核心状态属性 ===
|
# === 核心状态属性 ===
|
||||||
|
|||||||
@@ -2,10 +2,14 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
|
|
||||||
class VirtualSeparator:
|
class VirtualSeparator:
|
||||||
"""Virtual separator device for SeparateProtocol testing"""
|
"""Virtual separator device for SeparateProtocol testing"""
|
||||||
|
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
||||||
# 处理可能的不同调用方式
|
# 处理可能的不同调用方式
|
||||||
if device_id is None and "id" in kwargs:
|
if device_id is None and "id" in kwargs:
|
||||||
@@ -36,6 +40,9 @@ class VirtualSeparator:
|
|||||||
if key not in skip_keys and not hasattr(self, key):
|
if key not in skip_keys and not hasattr(self, key):
|
||||||
setattr(self, key, value)
|
setattr(self, key, value)
|
||||||
|
|
||||||
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
|
self._ros_node = ros_node
|
||||||
|
|
||||||
async def initialize(self) -> bool:
|
async def initialize(self) -> bool:
|
||||||
"""Initialize virtual separator"""
|
"""Initialize virtual separator"""
|
||||||
print(f"=== VirtualSeparator {self.device_id} initialize() called! ===")
|
print(f"=== VirtualSeparator {self.device_id} initialize() called! ===")
|
||||||
@@ -119,14 +126,14 @@ class VirtualSeparator:
|
|||||||
for repeat in range(repeats):
|
for repeat in range(repeats):
|
||||||
# 搅拌阶段
|
# 搅拌阶段
|
||||||
for progress in range(0, 51, 10):
|
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
|
overall_progress = ((repeat * 100) + (progress * 0.5)) / repeats
|
||||||
self.data["progress"] = overall_progress
|
self.data["progress"] = overall_progress
|
||||||
self.data["message"] = f"第{repeat+1}次分离 - 搅拌中 ({progress}%)"
|
self.data["message"] = f"第{repeat+1}次分离 - 搅拌中 ({progress}%)"
|
||||||
|
|
||||||
# 静置分相阶段
|
# 静置分相阶段
|
||||||
for progress in range(50, 101, 10):
|
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
|
overall_progress = ((repeat * 100) + (progress * 0.5)) / repeats
|
||||||
self.data["progress"] = overall_progress
|
self.data["progress"] = overall_progress
|
||||||
self.data["message"] = f"第{repeat+1}次分离 - 静置分相中 ({progress}%)"
|
self.data["message"] = f"第{repeat+1}次分离 - 静置分相中 ({progress}%)"
|
||||||
|
|||||||
@@ -2,11 +2,16 @@ import time
|
|||||||
import asyncio
|
import asyncio
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
|
|
||||||
class VirtualSolenoidValve:
|
class VirtualSolenoidValve:
|
||||||
"""
|
"""
|
||||||
虚拟电磁阀门 - 简单的开关型阀门,只有开启和关闭两个状态
|
虚拟电磁阀门 - 简单的开关型阀门,只有开启和关闭两个状态
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, device_id: str = None, config: dict = None, **kwargs):
|
def __init__(self, device_id: str = None, config: dict = None, **kwargs):
|
||||||
# 从配置中获取参数,提供默认值
|
# 从配置中获取参数,提供默认值
|
||||||
if config is None:
|
if config is None:
|
||||||
@@ -22,6 +27,9 @@ class VirtualSolenoidValve:
|
|||||||
self._valve_state = "Closed" # "Open" or "Closed"
|
self._valve_state = "Closed" # "Open" or "Closed"
|
||||||
self._is_open = False
|
self._is_open = False
|
||||||
|
|
||||||
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||||
|
self._ros_node = ros_node
|
||||||
|
|
||||||
async def initialize(self) -> bool:
|
async def initialize(self) -> bool:
|
||||||
"""初始化设备"""
|
"""初始化设备"""
|
||||||
self._status = "Idle"
|
self._status = "Idle"
|
||||||
@@ -63,7 +71,7 @@ class VirtualSolenoidValve:
|
|||||||
self._status = "Busy"
|
self._status = "Busy"
|
||||||
|
|
||||||
# 模拟阀门响应时间
|
# 模拟阀门响应时间
|
||||||
await asyncio.sleep(self.response_time)
|
await self._ros_node.sleep(self.response_time)
|
||||||
|
|
||||||
# 处理不同的命令格式
|
# 处理不同的命令格式
|
||||||
if isinstance(command, str):
|
if isinstance(command, str):
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import logging
|
|||||||
import re
|
import re
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
class VirtualSolidDispenser:
|
class VirtualSolidDispenser:
|
||||||
"""
|
"""
|
||||||
虚拟固体粉末加样器 - 用于处理 Add Protocol 中的固体试剂添加 ⚗️
|
虚拟固体粉末加样器 - 用于处理 Add Protocol 中的固体试剂添加 ⚗️
|
||||||
@@ -13,6 +15,8 @@ class VirtualSolidDispenser:
|
|||||||
- 简单反馈:成功/失败 + 消息 📊
|
- 简单反馈:成功/失败 + 消息 📊
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
|
def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
|
||||||
self.device_id = device_id or "virtual_solid_dispenser"
|
self.device_id = device_id or "virtual_solid_dispenser"
|
||||||
self.config = config or {}
|
self.config = config or {}
|
||||||
@@ -32,6 +36,9 @@ class VirtualSolidDispenser:
|
|||||||
print(f"⚗️ === 虚拟固体分配器 {self.device_id} 创建成功! === ✨")
|
print(f"⚗️ === 虚拟固体分配器 {self.device_id} 创建成功! === ✨")
|
||||||
print(f"📊 设备规格: 最大容量 {self.max_capacity}g | 精度 {self.precision}g 🎯")
|
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:
|
async def initialize(self) -> bool:
|
||||||
"""初始化固体加样器 🚀"""
|
"""初始化固体加样器 🚀"""
|
||||||
self.logger.info(f"🔧 初始化固体分配器 {self.device_id} ✨")
|
self.logger.info(f"🔧 初始化固体分配器 {self.device_id} ✨")
|
||||||
@@ -263,7 +270,7 @@ class VirtualSolidDispenser:
|
|||||||
|
|
||||||
for i in range(steps):
|
for i in range(steps):
|
||||||
progress = (i + 1) / steps * 100
|
progress = (i + 1) / steps * 100
|
||||||
await asyncio.sleep(step_time)
|
await self._ros_node.sleep(step_time)
|
||||||
if i % 2 == 0: # 每隔一步显示进度
|
if i % 2 == 0: # 每隔一步显示进度
|
||||||
self.logger.debug(f"📊 加样进度: {progress:.0f}% | {amount_emoji} 正在分配 {reagent}...")
|
self.logger.debug(f"📊 加样进度: {progress:.0f}% | {amount_emoji} 正在分配 {reagent}...")
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,13 @@ import logging
|
|||||||
import time as time_module
|
import time as time_module
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
class VirtualStirrer:
|
class VirtualStirrer:
|
||||||
"""Virtual stirrer device for StirProtocol testing - 功能完整版 🌪️"""
|
"""Virtual stirrer device for StirProtocol testing - 功能完整版 🌪️"""
|
||||||
|
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
|
def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
|
||||||
# 处理可能的不同调用方式
|
# 处理可能的不同调用方式
|
||||||
if device_id is None and 'id' in kwargs:
|
if device_id is None and 'id' in kwargs:
|
||||||
@@ -34,6 +38,9 @@ class VirtualStirrer:
|
|||||||
print(f"🌪️ === 虚拟搅拌器 {self.device_id} 已创建 === ✨")
|
print(f"🌪️ === 虚拟搅拌器 {self.device_id} 已创建 === ✨")
|
||||||
print(f"🔧 速度范围: {self._min_speed} ~ {self._max_speed} RPM | 📱 端口: {self.port}")
|
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:
|
async def initialize(self) -> bool:
|
||||||
"""Initialize virtual stirrer 🚀"""
|
"""Initialize virtual stirrer 🚀"""
|
||||||
self.logger.info(f"🔧 初始化虚拟搅拌器 {self.device_id} ✨")
|
self.logger.info(f"🔧 初始化虚拟搅拌器 {self.device_id} ✨")
|
||||||
@@ -134,7 +141,7 @@ class VirtualStirrer:
|
|||||||
if remaining <= 0:
|
if remaining <= 0:
|
||||||
break
|
break
|
||||||
|
|
||||||
await asyncio.sleep(1.0)
|
await self._ros_node.sleep(1.0)
|
||||||
|
|
||||||
self.logger.info(f"✅ 搅拌阶段完成! 🌪️ {stir_speed} RPM × {stir_time}s")
|
self.logger.info(f"✅ 搅拌阶段完成! 🌪️ {stir_speed} RPM × {stir_time}s")
|
||||||
|
|
||||||
@@ -176,7 +183,7 @@ class VirtualStirrer:
|
|||||||
if remaining <= 0:
|
if remaining <= 0:
|
||||||
break
|
break
|
||||||
|
|
||||||
await asyncio.sleep(1.0)
|
await self._ros_node.sleep(1.0)
|
||||||
|
|
||||||
self.logger.info(f"✅ 沉降阶段完成! 🛑 静置 {settling_time}s")
|
self.logger.info(f"✅ 沉降阶段完成! 🛑 静置 {settling_time}s")
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ from enum import Enum
|
|||||||
from typing import Union, Optional
|
from typing import Union, Optional
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||||
|
|
||||||
|
|
||||||
class VirtualPumpMode(Enum):
|
class VirtualPumpMode(Enum):
|
||||||
Normal = 0
|
Normal = 0
|
||||||
@@ -14,6 +16,8 @@ class VirtualPumpMode(Enum):
|
|||||||
class VirtualTransferPump:
|
class VirtualTransferPump:
|
||||||
"""虚拟转移泵类 - 模拟泵的基本功能,无需实际硬件 🚰"""
|
"""虚拟转移泵类 - 模拟泵的基本功能,无需实际硬件 🚰"""
|
||||||
|
|
||||||
|
_ros_node: BaseROS2DeviceNode
|
||||||
|
|
||||||
def __init__(self, device_id: str = None, config: dict = None, **kwargs):
|
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"💨 快速模式: {'启用' if self._fast_mode else '禁用'} | 移动时间: {self._fast_move_time}s | 喷射时间: {self._fast_dispense_time}s")
|
||||||
print(f"📊 最大容量: {self.max_volume}mL | 端口: {self.port}")
|
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:
|
async def initialize(self) -> bool:
|
||||||
"""初始化虚拟泵 🚀"""
|
"""初始化虚拟泵 🚀"""
|
||||||
self.logger.info(f"🔧 初始化虚拟转移泵 {self.device_id} ✨")
|
self.logger.info(f"🔧 初始化虚拟转移泵 {self.device_id} ✨")
|
||||||
@@ -104,7 +111,7 @@ class VirtualTransferPump:
|
|||||||
async def _simulate_operation(self, duration: float):
|
async def _simulate_operation(self, duration: float):
|
||||||
"""模拟操作延时 ⏱️"""
|
"""模拟操作延时 ⏱️"""
|
||||||
self._status = "Busy"
|
self._status = "Busy"
|
||||||
await asyncio.sleep(duration)
|
await self._ros_node.sleep(duration)
|
||||||
self._status = "Idle"
|
self._status = "Idle"
|
||||||
|
|
||||||
def _calculate_duration(self, volume: float, velocity: float = None) -> float:
|
def _calculate_duration(self, volume: float, velocity: float = None) -> float:
|
||||||
@@ -223,7 +230,7 @@ class VirtualTransferPump:
|
|||||||
|
|
||||||
# 等待一小步时间
|
# 等待一小步时间
|
||||||
if i < steps and step_duration > 0:
|
if i < steps and step_duration > 0:
|
||||||
await asyncio.sleep(step_duration)
|
await self._ros_node.sleep(step_duration)
|
||||||
else:
|
else:
|
||||||
# 移动距离很小,直接完成
|
# 移动距离很小,直接完成
|
||||||
self._position = target_position
|
self._position = target_position
|
||||||
@@ -341,7 +348,7 @@ class VirtualTransferPump:
|
|||||||
|
|
||||||
# 短暂停顿
|
# 短暂停顿
|
||||||
self.logger.debug("⏸️ 短暂停顿...")
|
self.logger.debug("⏸️ 短暂停顿...")
|
||||||
await asyncio.sleep(0.1)
|
await self._ros_node.sleep(0.1)
|
||||||
|
|
||||||
# 排液
|
# 排液
|
||||||
await self.dispense(volume, dispense_velocity)
|
await self.dispense(volume, dispense_velocity)
|
||||||
|
|||||||
Binary file not shown.
@@ -3,6 +3,7 @@ from cgi import print_arguments
|
|||||||
from doctest import debug
|
from doctest import debug
|
||||||
from typing import Dict, Any, List, Optional
|
from typing import Dict, Any, List, Optional
|
||||||
import requests
|
import requests
|
||||||
|
from pylabrobot.resources.resource import Resource as ResourcePLR
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import time
|
import time
|
||||||
@@ -254,7 +255,7 @@ class BioyondCellWorkstation(BioyondWorkstation):
|
|||||||
def auto_feeding4to3(
|
def auto_feeding4to3(
|
||||||
self,
|
self,
|
||||||
# ★ 修改点:默认模板路径
|
# ★ 修改点:默认模板路径
|
||||||
xlsx_path: Optional[str] = "C:/ML/GitHub/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx",
|
xlsx_path: Optional[str] = "/Users/sml/work/Unilab/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx",
|
||||||
# ---------------- WH4 - 加样头面 (Z=1, 12个点位) ----------------
|
# ---------------- WH4 - 加样头面 (Z=1, 12个点位) ----------------
|
||||||
WH4_x1_y1_z1_1_materialName: str = "", WH4_x1_y1_z1_1_quantity: float = 0.0,
|
WH4_x1_y1_z1_1_materialName: str = "", WH4_x1_y1_z1_1_quantity: float = 0.0,
|
||||||
WH4_x2_y1_z1_2_materialName: str = "", WH4_x2_y1_z1_2_quantity: float = 0.0,
|
WH4_x2_y1_z1_2_materialName: str = "", WH4_x2_y1_z1_2_quantity: float = 0.0,
|
||||||
@@ -306,7 +307,7 @@ class BioyondCellWorkstation(BioyondWorkstation):
|
|||||||
|
|
||||||
# ---------- 模式 1: Excel 导入 ----------
|
# ---------- 模式 1: Excel 导入 ----------
|
||||||
if xlsx_path:
|
if xlsx_path:
|
||||||
path = Path(xlsx_path)
|
path = Path(__file__).parent / Path(xlsx_path)
|
||||||
if path.exists(): # ★ 修改点:路径存在才加载
|
if path.exists(): # ★ 修改点:路径存在才加载
|
||||||
try:
|
try:
|
||||||
df = pd.read_excel(path, sheet_name=0, header=None, engine="openpyxl")
|
df = pd.read_excel(path, sheet_name=0, header=None, engine="openpyxl")
|
||||||
@@ -471,14 +472,23 @@ class BioyondCellWorkstation(BioyondWorkstation):
|
|||||||
- totalMass 自动计算为所有物料质量之和
|
- totalMass 自动计算为所有物料质量之和
|
||||||
- createTime 缺失或为空时自动填充为当前日期(YYYY/M/D)
|
- createTime 缺失或为空时自动填充为当前日期(YYYY/M/D)
|
||||||
"""
|
"""
|
||||||
path = Path(xlsx_path)
|
default_path = Path("/Users/sml/work/Unilab/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/2025092701.xlsx")
|
||||||
|
path = Path(xlsx_path) if xlsx_path else default_path
|
||||||
|
print(f"[create_orders] 使用 Excel 路径: {path}")
|
||||||
|
if path != default_path:
|
||||||
|
print("[create_orders] 来源: 调用方传入自定义路径")
|
||||||
|
else:
|
||||||
|
print("[create_orders] 来源: 使用默认模板路径")
|
||||||
|
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
|
print(f"[create_orders] ⚠️ Excel 文件不存在: {path}")
|
||||||
raise FileNotFoundError(f"未找到 Excel 文件:{path}")
|
raise FileNotFoundError(f"未找到 Excel 文件:{path}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
df = pd.read_excel(path, sheet_name=0, engine="openpyxl")
|
df = pd.read_excel(path, sheet_name=0, engine="openpyxl")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise RuntimeError(f"读取 Excel 失败:{e}")
|
raise RuntimeError(f"读取 Excel 失败:{e}")
|
||||||
|
print(f"[create_orders] Excel 读取成功,行数: {len(df)}, 列: {list(df.columns)}")
|
||||||
|
|
||||||
# 列名容错:返回可选列名,找不到则返回 None
|
# 列名容错:返回可选列名,找不到则返回 None
|
||||||
def _pick(col_names: List[str]) -> Optional[str]:
|
def _pick(col_names: List[str]) -> Optional[str]:
|
||||||
@@ -495,9 +505,20 @@ class BioyondCellWorkstation(BioyondWorkstation):
|
|||||||
col_pouch = _pick(["软包组装分液体积", "pouchCellInfo"])
|
col_pouch = _pick(["软包组装分液体积", "pouchCellInfo"])
|
||||||
col_cond = _pick(["电导测试分液体积", "conductivityInfo"])
|
col_cond = _pick(["电导测试分液体积", "conductivityInfo"])
|
||||||
col_cond_cnt = _pick(["电导测试分液瓶数", "conductivityBottleCount"])
|
col_cond_cnt = _pick(["电导测试分液瓶数", "conductivityBottleCount"])
|
||||||
|
print("[create_orders] 列匹配结果:", {
|
||||||
|
"order_name": col_order_name,
|
||||||
|
"create_time": col_create_time,
|
||||||
|
"bottle_type": col_bottle_type,
|
||||||
|
"mix_time": col_mix_time,
|
||||||
|
"load": col_load,
|
||||||
|
"pouch": col_pouch,
|
||||||
|
"conductivity": col_cond,
|
||||||
|
"conductivity_bottle_count": col_cond_cnt,
|
||||||
|
})
|
||||||
|
|
||||||
# 物料列:所有以 (g) 结尾
|
# 物料列:所有以 (g) 结尾
|
||||||
material_cols = [c for c in df.columns if isinstance(c, str) and c.endswith("(g)")]
|
material_cols = [c for c in df.columns if isinstance(c, str) and c.endswith("(g)")]
|
||||||
|
print(f"[create_orders] 识别到的物料列: {material_cols}")
|
||||||
if not material_cols:
|
if not material_cols:
|
||||||
raise KeyError("未发现任何以“(g)”结尾的物料列,请检查表头。")
|
raise KeyError("未发现任何以“(g)”结尾的物料列,请检查表头。")
|
||||||
|
|
||||||
@@ -545,6 +566,9 @@ class BioyondCellWorkstation(BioyondWorkstation):
|
|||||||
if mass > 0:
|
if mass > 0:
|
||||||
mats.append({"name": mcol.replace("(g)", ""), "mass": mass})
|
mats.append({"name": mcol.replace("(g)", ""), "mass": mass})
|
||||||
total_mass += mass
|
total_mass += mass
|
||||||
|
else:
|
||||||
|
if mass < 0:
|
||||||
|
print(f"[create_orders] 第 {idx+1} 行物料 {mcol} 数值为负数: {mass}")
|
||||||
|
|
||||||
order_data = {
|
order_data = {
|
||||||
"batchId": batch_id,
|
"batchId": batch_id,
|
||||||
@@ -559,11 +583,22 @@ class BioyondCellWorkstation(BioyondWorkstation):
|
|||||||
"materialInfos": mats,
|
"materialInfos": mats,
|
||||||
"totalMass": round(total_mass, 4) # 自动汇总
|
"totalMass": round(total_mass, 4) # 自动汇总
|
||||||
}
|
}
|
||||||
|
print(f"[create_orders] 第 {idx+1} 行解析结果: orderName={order_data['orderName']}, "
|
||||||
|
f"loadShedding={order_data['loadSheddingInfo']}, pouchCell={order_data['pouchCellInfo']}, "
|
||||||
|
f"conductivity={order_data['conductivityInfo']}, totalMass={order_data['totalMass']}, "
|
||||||
|
f"material_count={len(mats)}")
|
||||||
|
|
||||||
|
if order_data["totalMass"] <= 0:
|
||||||
|
print(f"[create_orders] ⚠️ 第 {idx+1} 行总质量 <= 0,可能导致 LIMS 校验失败")
|
||||||
|
if not mats:
|
||||||
|
print(f"[create_orders] ⚠️ 第 {idx+1} 行未找到有效物料")
|
||||||
|
|
||||||
orders.append(order_data)
|
orders.append(order_data)
|
||||||
|
|
||||||
|
|
||||||
|
print(f"[create_orders] 即将提交订单数量: {len(orders)}")
|
||||||
response = self._post_lims("/api/lims/order/orders", orders)
|
response = self._post_lims("/api/lims/order/orders", orders)
|
||||||
print(response)
|
print(f"[create_orders] 接口返回: {response}")
|
||||||
# 等待任务报送成功
|
# 等待任务报送成功
|
||||||
data_list = response.get("data", [])
|
data_list = response.get("data", [])
|
||||||
if data_list:
|
if data_list:
|
||||||
@@ -1014,7 +1049,35 @@ class BioyondCellWorkstation(BioyondWorkstation):
|
|||||||
"create_result": create_result,
|
"create_result": create_result,
|
||||||
"inbound_result": inbound_result,
|
"inbound_result": inbound_result,
|
||||||
}
|
}
|
||||||
|
def resource_tree_transfer(self, old_parent: ResourcePLR, plr_resource: ResourcePLR, parent_resource: ResourcePLR):
|
||||||
|
# ROS2DeviceNode.run_async_func(self._ros_node.resource_tree_transfer, True, **{
|
||||||
|
# "old_parent": old_parent,
|
||||||
|
# "plr_resource": plr_resource,
|
||||||
|
# "parent_resource": parent_resource,
|
||||||
|
# })
|
||||||
|
print("resource_tree_transfer", plr_resource, parent_resource)
|
||||||
|
if hasattr(plr_resource, "unilabos_extra") and plr_resource.unilabos_extra:
|
||||||
|
if "update_resource_site" in plr_resource.unilabos_extra:
|
||||||
|
site = plr_resource.unilabos_extra["update_resource_site"]
|
||||||
|
plr_model = plr_resource.model
|
||||||
|
board_type = None
|
||||||
|
for key, (moudle_name,moudle_uuid) in MATERIAL_TYPE_MAPPINGS.items():
|
||||||
|
if plr_model == moudle_name:
|
||||||
|
board_type = key
|
||||||
|
break
|
||||||
|
if board_type is None:
|
||||||
|
pass
|
||||||
|
bottle1 = plr_resource.children[0]
|
||||||
|
|
||||||
|
bottle_moudle = bottle1.model
|
||||||
|
bottle_type = None
|
||||||
|
for key, (moudle_name, moudle_uuid) in MATERIAL_TYPE_MAPPINGS.items():
|
||||||
|
if bottle_moudle == moudle_name:
|
||||||
|
bottle_type = key
|
||||||
|
break
|
||||||
|
self.create_sample(plr_resource.name, board_type,bottle_type,site)
|
||||||
|
return
|
||||||
|
self.lab_logger().warning(f"无库位的上料,不处理,{plr_resource} 挂载到 {parent_resource}")
|
||||||
|
|
||||||
def create_sample(
|
def create_sample(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import os
|
|||||||
# BioyondCellWorkstation 默认配置(包含所有必需参数)
|
# BioyondCellWorkstation 默认配置(包含所有必需参数)
|
||||||
API_CONFIG = {
|
API_CONFIG = {
|
||||||
# API 连接配置
|
# API 连接配置
|
||||||
# "api_host": os.getenv("BIOYOND_API_HOST", "http://172.21.32.65:44389"),#实机
|
# "api_host": os.getenv("BIOYOND_API_HOST", "http://172.16.1.143:44389"),#实机
|
||||||
"api_host": os.getenv("BIOYOND_API_HOST", "http://172.16.10.169:44388"),# 仿真机
|
"api_host": os.getenv("BIOYOND_API_HOST", "http://172.16.7.149:44388"),# 仿真机
|
||||||
"api_key": os.getenv("BIOYOND_API_KEY", "8A819E5C"),
|
"api_key": os.getenv("BIOYOND_API_KEY", "8A819E5C"),
|
||||||
"timeout": int(os.getenv("BIOYOND_TIMEOUT", "30")),
|
"timeout": int(os.getenv("BIOYOND_TIMEOUT", "30")),
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ API_CONFIG = {
|
|||||||
"report_token": os.getenv("BIOYOND_REPORT_TOKEN", "CHANGE_ME_TOKEN"),
|
"report_token": os.getenv("BIOYOND_REPORT_TOKEN", "CHANGE_ME_TOKEN"),
|
||||||
|
|
||||||
# HTTP 服务配置
|
# HTTP 服务配置
|
||||||
"HTTP_host": os.getenv("BIOYOND_HTTP_HOST", "172.21.32.115"), # HTTP服务监听地址,监听计算机飞连ip地址
|
"HTTP_host": os.getenv("BIOYOND_HTTP_HOST", "172.16.2.140"), # HTTP服务监听地址,监听计算机飞连ip地址
|
||||||
"HTTP_port": int(os.getenv("BIOYOND_HTTP_PORT", "8080")),
|
"HTTP_port": int(os.getenv("BIOYOND_HTTP_PORT", "8080")),
|
||||||
"debug_mode": False,# 调试模式
|
"debug_mode": False,# 调试模式
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,7 +177,18 @@ class BioyondWorkstation(WorkstationBase):
|
|||||||
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
|
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
|
||||||
"resources": [self.deck]
|
"resources": [self.deck]
|
||||||
})
|
})
|
||||||
|
def resource_tree_transfer(self, old_parent: ResourcePLR, plr_resource: ResourcePLR, parent_resource: ResourcePLR):
|
||||||
|
# ROS2DeviceNode.run_async_func(self._ros_node.resource_tree_transfer, True, **{
|
||||||
|
# "old_parent": old_parent,
|
||||||
|
# "plr_resource": plr_resource,
|
||||||
|
# "parent_resource": parent_resource,
|
||||||
|
# })
|
||||||
|
print("resource_tree_transfer", plr_resource, parent_resource)
|
||||||
|
if hasattr(plr_resource, "unilabos_data") and plr_resource.unilabos_data:
|
||||||
|
if "update_resource_site" in plr_resource.unilabos_data:
|
||||||
|
site = plr_resource.unilabos_data["update_resource_site"]
|
||||||
|
return
|
||||||
|
self.lab_logger().warning(f"无库位的上料,不处理,{plr_resource} 挂载到 {parent_resource}")
|
||||||
def transfer_resource_to_another(self, resource: List[ResourceSlot], mount_resource: List[ResourceSlot], sites: List[str], mount_device_id: DeviceSlot):
|
def transfer_resource_to_another(self, resource: List[ResourceSlot], mount_resource: List[ResourceSlot], sites: List[str], mount_device_id: DeviceSlot):
|
||||||
ROS2DeviceNode.run_async_func(self._ros_node.transfer_resource_to_another, True, **{
|
ROS2DeviceNode.run_async_func(self._ros_node.transfer_resource_to_another, True, **{
|
||||||
"plr_resources": resource,
|
"plr_resources": resource,
|
||||||
|
|||||||
Binary file not shown.
@@ -112,7 +112,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
deck: Deck=None,
|
deck: Deck=None,
|
||||||
address: str = "172.21.33.176",
|
address: str = "172.16.28.102",
|
||||||
port: str = "502",
|
port: str = "502",
|
||||||
debug_mode: bool = False,
|
debug_mode: bool = False,
|
||||||
*args,
|
*args,
|
||||||
@@ -165,7 +165,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
|||||||
print("测试模式,跳过连接")
|
print("测试模式,跳过连接")
|
||||||
|
|
||||||
""" 工站的配置 """
|
""" 工站的配置 """
|
||||||
self.nodes = BaseClient.load_csv(os.path.join(os.path.dirname(__file__), 'coin_cell_assembly_a.csv'))
|
self.nodes = BaseClient.load_csv(os.path.join(os.path.dirname(__file__), 'coin_cell_assembly_1105.csv'))
|
||||||
self.client = modbus_client.register_node_list(self.nodes)
|
self.client = modbus_client.register_node_list(self.nodes)
|
||||||
self.success = False
|
self.success = False
|
||||||
self.allow_data_read = False #允许读取函数运行标志位
|
self.allow_data_read = False #允许读取函数运行标志位
|
||||||
@@ -906,7 +906,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
|||||||
|
|
||||||
return self.success
|
return self.success
|
||||||
|
|
||||||
def func_allpack_cmd(self, elec_num, elec_use_num, elec_vol:int=50, assembly_type:int=7, assembly_pressure:int=4200, file_path: str="C:\\Users\\67484\\Desktop") -> bool:
|
def func_allpack_cmd(self, elec_num, elec_use_num, elec_vol:int=50, assembly_type:int=7, assembly_pressure:int=4200, file_path: str="/Users/sml/work") -> bool:
|
||||||
elec_num, elec_use_num, elec_vol, assembly_type, assembly_pressure = int(elec_num), int(elec_use_num), int(elec_vol), int(assembly_type), int(assembly_pressure)
|
elec_num, elec_use_num, elec_vol, assembly_type, assembly_pressure = int(elec_num), int(elec_use_num), int(elec_vol), int(assembly_type), int(assembly_pressure)
|
||||||
summary_csv_file = os.path.join(file_path, "duandian.csv")
|
summary_csv_file = os.path.join(file_path, "duandian.csv")
|
||||||
# 如果断点文件存在,先读取之前的进度
|
# 如果断点文件存在,先读取之前的进度
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
Name,DataType,InitValue,Comment,Attribute,DeviceType,Address,
|
||||||
|
COIL_SYS_START_CMD,BOOL,,,,coil,9010,
|
||||||
|
COIL_SYS_STOP_CMD,BOOL,,,,coil,9020,
|
||||||
|
COIL_SYS_RESET_CMD,BOOL,,,,coil,9030,
|
||||||
|
COIL_SYS_HAND_CMD,BOOL,,,,coil,9040,
|
||||||
|
COIL_SYS_AUTO_CMD,BOOL,,,,coil,9050,
|
||||||
|
COIL_SYS_INIT_CMD,BOOL,,,,coil,9060,
|
||||||
|
COIL_UNILAB_SEND_MSG_SUCC_CMD,BOOL,,,,coil,9700,
|
||||||
|
COIL_UNILAB_REC_MSG_SUCC_CMD,BOOL,,,,coil,9710,unilab_rec_msg_succ_cmd
|
||||||
|
COIL_SYS_START_STATUS,BOOL,,,,coil,9210,
|
||||||
|
COIL_SYS_STOP_STATUS,BOOL,,,,coil,9220,
|
||||||
|
COIL_SYS_RESET_STATUS,BOOL,,,,coil,9230,
|
||||||
|
COIL_SYS_HAND_STATUS,BOOL,,,,coil,9240,
|
||||||
|
COIL_SYS_AUTO_STATUS,BOOL,,,,coil,9250,
|
||||||
|
COIL_SYS_INIT_STATUS,BOOL,,,,coil,9260,
|
||||||
|
COIL_REQUEST_REC_MSG_STATUS,BOOL,,,,coil,9500,
|
||||||
|
COIL_REQUEST_SEND_MSG_STATUS,BOOL,,,,coil,9510,request_send_msg_status
|
||||||
|
REG_MSG_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,17000,
|
||||||
|
REG_MSG_ELECTROLYTE_NUM,INT16,,,,hold_register,17002,unilab_send_msg_electrolyte_num
|
||||||
|
REG_MSG_ELECTROLYTE_VOLUME,INT16,,,,hold_register,17004,unilab_send_msg_electrolyte_vol
|
||||||
|
REG_MSG_ASSEMBLY_TYPE,INT16,,,,hold_register,17006,unilab_send_msg_assembly_type
|
||||||
|
REG_MSG_ASSEMBLY_PRESSURE,INT16,,,,hold_register,17008,unilab_send_msg_assembly_pressure
|
||||||
|
REG_DATA_ASSEMBLY_COIN_CELL_NUM,INT16,,,,hold_register,16000,data_assembly_coin_cell_num
|
||||||
|
REG_DATA_OPEN_CIRCUIT_VOLTAGE,FLOAT32,,,,hold_register,16002,data_open_circuit_voltage
|
||||||
|
REG_DATA_AXIS_X_POS,FLOAT32,,,,hold_register,16004,
|
||||||
|
REG_DATA_AXIS_Y_POS,FLOAT32,,,,hold_register,16006,
|
||||||
|
REG_DATA_AXIS_Z_POS,FLOAT32,,,,hold_register,16008,
|
||||||
|
REG_DATA_POLE_WEIGHT,FLOAT32,,,,hold_register,16010,data_pole_weight
|
||||||
|
REG_DATA_ASSEMBLY_PER_TIME,FLOAT32,,,,hold_register,16012,data_assembly_time
|
||||||
|
REG_DATA_ASSEMBLY_PRESSURE,INT16,,,,hold_register,16014,data_assembly_pressure
|
||||||
|
REG_DATA_ELECTROLYTE_VOLUME,INT16,,,,hold_register,16016,data_electrolyte_volume
|
||||||
|
REG_DATA_COIN_NUM,INT16,,,,hold_register,16018,data_coin_num
|
||||||
|
REG_DATA_ELECTROLYTE_CODE,STRING,,,,hold_register,16020,data_electrolyte_code()
|
||||||
|
REG_DATA_COIN_CELL_CODE,STRING,,,,hold_register,16030,data_coin_cell_code()
|
||||||
|
REG_DATA_STACK_VISON_CODE,STRING,,,,hold_register,18004,data_stack_vision_code()
|
||||||
|
REG_DATA_GLOVE_BOX_PRESSURE,FLOAT32,,,,hold_register,16050,data_glove_box_pressure
|
||||||
|
REG_DATA_GLOVE_BOX_WATER_CONTENT,FLOAT32,,,,hold_register,16052,data_glove_box_water_content
|
||||||
|
REG_DATA_GLOVE_BOX_O2_CONTENT,FLOAT32,,,,hold_register,16054,data_glove_box_o2_content
|
||||||
|
UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,9720,
|
||||||
|
UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,9520,
|
||||||
|
REG_MSG_ELECTROLYTE_NUM_USED,INT16,,,,hold_register,17496,
|
||||||
|
REG_DATA_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,16000,
|
||||||
|
UNILAB_SEND_FINISHED_CMD,BOOL,,,,coil,9730,
|
||||||
|
UNILAB_RECE_FINISHED_CMD,BOOL,,,,coil,9530,
|
||||||
|
REG_DATA_ASSEMBLY_TYPE,INT16,,,,hold_register,16018,ASSEMBLY_TYPE7or8
|
||||||
|
COIL_ALUMINUM_FOIL,BOOL,,使用铝箔垫,,coil,9340,
|
||||||
|
REG_MSG_NE_PLATE_MATRIX,INT16,,负极片矩阵点位,,hold_register,17440,
|
||||||
|
REG_MSG_SEPARATOR_PLATE_MATRIX,INT16,,隔膜矩阵点位,,hold_register,17450,
|
||||||
|
REG_MSG_TIP_BOX_MATRIX,INT16,,移液枪头矩阵点位,,hold_register,17480,
|
||||||
|
REG_MSG_NE_PLATE_NUM,INT16,,负极片盘数,,hold_register,17443,
|
||||||
|
REG_MSG_SEPARATOR_PLATE_NUM,INT16,,隔膜盘数,,hold_register,17453,
|
||||||
|
REG_MSG_PRESS_MODE,BOOL,,压制模式(false:压力检测模式,True:距离模式),,coil,9360,电池压制模式
|
||||||
|
,,,,,,,
|
||||||
|
,BOOL,,视觉对位(false:使用,true:忽略),,coil,9300,视觉对位
|
||||||
|
,BOOL,,复检(false:使用,true:忽略),,coil,9310,视觉复检
|
||||||
|
,BOOL,,手套箱_左仓(false:使用,true:忽略),,coil,9320,手套箱左仓
|
||||||
|
,BOOL,,手套箱_右仓(false:使用,true:忽略),,coil,9420,手套箱右仓
|
||||||
|
,BOOL,,真空检知(false:使用,true:忽略),,coil,9350,真空检知
|
||||||
|
,BOOL,,电解液添加模式(false:单次滴液,true:二次滴液),,coil,9370,滴液模式
|
||||||
|
,BOOL,,正极片称重(false:使用,true:忽略),,coil,9380,正极片称重
|
||||||
|
,BOOL,,正负极片组装方式(false:正装,true:倒装),,coil,9390,正负极反装
|
||||||
|
,BOOL,,压制清洁(false:使用,true:忽略),,coil,9400,压制清洁
|
||||||
|
,BOOL,,物料盘摆盘方式(false:水平摆盘,true:堆叠摆盘),,coil,9410,负极片摆盘方式
|
||||||
|
REG_MSG_BATTERY_CLEAN_IGNORE,BOOL,,忽略电池清洁(false:使用,true:忽略),,coil,9460,
|
||||||
|
@@ -233,7 +233,7 @@ def YB_1BottleCarrier(name: str) -> BottleCarrier:
|
|||||||
resource_size_y=beaker_diameter,
|
resource_size_y=beaker_diameter,
|
||||||
name_prefix=name,
|
name_prefix=name,
|
||||||
),
|
),
|
||||||
model="1BottleCarrier",
|
model="YB_1BottleCarrier",
|
||||||
)
|
)
|
||||||
carrier.num_items_x = 1
|
carrier.num_items_x = 1
|
||||||
carrier.num_items_y = 1
|
carrier.num_items_y = 1
|
||||||
@@ -270,7 +270,7 @@ def YB_1GaoNianYeBottleCarrier(name: str) -> BottleCarrier:
|
|||||||
resource_size_y=beaker_diameter,
|
resource_size_y=beaker_diameter,
|
||||||
name_prefix=name,
|
name_prefix=name,
|
||||||
),
|
),
|
||||||
model="1GaoNianYeBottleCarrier",
|
model="YB_1GaoNianYeBottleCarrier",
|
||||||
)
|
)
|
||||||
carrier.num_items_x = 1
|
carrier.num_items_x = 1
|
||||||
carrier.num_items_y = 1
|
carrier.num_items_y = 1
|
||||||
@@ -307,7 +307,7 @@ def YB_1Bottle100mlCarrier(name: str) -> BottleCarrier:
|
|||||||
resource_size_y=beaker_diameter,
|
resource_size_y=beaker_diameter,
|
||||||
name_prefix=name,
|
name_prefix=name,
|
||||||
),
|
),
|
||||||
model="1Bottle100mlCarrier",
|
model="YB_1Bottle100mlCarrier",
|
||||||
)
|
)
|
||||||
carrier.num_items_x = 1
|
carrier.num_items_x = 1
|
||||||
carrier.num_items_y = 1
|
carrier.num_items_y = 1
|
||||||
@@ -355,7 +355,7 @@ def YB_6x5ml_DispensingVialCarrier(name: str) -> BottleCarrier:
|
|||||||
size_y=carrier_size_y,
|
size_y=carrier_size_y,
|
||||||
size_z=carrier_size_z,
|
size_z=carrier_size_z,
|
||||||
sites=sites,
|
sites=sites,
|
||||||
model="6x5ml_DispensingVialCarrier",
|
model="YB_6x5ml_DispensingVialCarrier",
|
||||||
)
|
)
|
||||||
carrier.num_items_x = 4
|
carrier.num_items_x = 4
|
||||||
carrier.num_items_y = 2
|
carrier.num_items_y = 2
|
||||||
@@ -405,7 +405,7 @@ def YB_6x20ml_DispensingVialCarrier(name: str) -> BottleCarrier:
|
|||||||
size_y=carrier_size_y,
|
size_y=carrier_size_y,
|
||||||
size_z=carrier_size_z,
|
size_z=carrier_size_z,
|
||||||
sites=sites,
|
sites=sites,
|
||||||
model="6x20ml_DispensingVialCarrier",
|
model="YB_6x20ml_DispensingVialCarrier",
|
||||||
)
|
)
|
||||||
carrier.num_items_x = 4
|
carrier.num_items_x = 4
|
||||||
carrier.num_items_y = 2
|
carrier.num_items_y = 2
|
||||||
@@ -455,7 +455,7 @@ def YB_6x_SmallSolutionBottleCarrier(name: str) -> BottleCarrier:
|
|||||||
size_y=carrier_size_y,
|
size_y=carrier_size_y,
|
||||||
size_z=carrier_size_z,
|
size_z=carrier_size_z,
|
||||||
sites=sites,
|
sites=sites,
|
||||||
model="6x_SmallSolutionBottleCarrier",
|
model="YB_6x_SmallSolutionBottleCarrier",
|
||||||
)
|
)
|
||||||
carrier.num_items_x = 4
|
carrier.num_items_x = 4
|
||||||
carrier.num_items_y = 2
|
carrier.num_items_y = 2
|
||||||
@@ -505,7 +505,7 @@ def YB_4x_LargeSolutionBottleCarrier(name: str) -> BottleCarrier:
|
|||||||
size_y=carrier_size_y,
|
size_y=carrier_size_y,
|
||||||
size_z=carrier_size_z,
|
size_z=carrier_size_z,
|
||||||
sites=sites,
|
sites=sites,
|
||||||
model="4x_LargeSolutionBottleCarrier",
|
model="YB_4x_LargeSolutionBottleCarrier",
|
||||||
)
|
)
|
||||||
carrier.num_items_x = 2
|
carrier.num_items_x = 2
|
||||||
carrier.num_items_y = 2
|
carrier.num_items_y = 2
|
||||||
@@ -554,7 +554,7 @@ def YB_jia_yang_tou_da_1X1_carrier(name: str) -> BottleCarrier:
|
|||||||
size_y=carrier_size_y,
|
size_y=carrier_size_y,
|
||||||
size_z=carrier_size_z,
|
size_z=carrier_size_z,
|
||||||
sites=sites,
|
sites=sites,
|
||||||
model="6x_LargeDispenseHeadCarrier",
|
model="YB_6x_LargeDispenseHeadCarrier",
|
||||||
)
|
)
|
||||||
carrier.num_items_x = 1
|
carrier.num_items_x = 1
|
||||||
carrier.num_items_y = 1
|
carrier.num_items_y = 1
|
||||||
@@ -591,7 +591,7 @@ def YB_AdapterBlock(name: str) -> BottleCarrier:
|
|||||||
resource_size_y=adapter_diameter,
|
resource_size_y=adapter_diameter,
|
||||||
name_prefix=name,
|
name_prefix=name,
|
||||||
),
|
),
|
||||||
model="AdapterBlock",
|
model="YB_AdapterBlock",
|
||||||
)
|
)
|
||||||
carrier.num_items_x = 1
|
carrier.num_items_x = 1
|
||||||
carrier.num_items_y = 1
|
carrier.num_items_y = 1
|
||||||
@@ -639,7 +639,7 @@ def YB_TipBox(name: str) -> BottleCarrier:
|
|||||||
size_y=carrier_size_y,
|
size_y=carrier_size_y,
|
||||||
size_z=carrier_size_z,
|
size_z=carrier_size_z,
|
||||||
sites=sites,
|
sites=sites,
|
||||||
model="TipBox",
|
model="YB_TipBox",
|
||||||
)
|
)
|
||||||
carrier.num_items_x = 12
|
carrier.num_items_x = 12
|
||||||
carrier.num_items_y = 8
|
carrier.num_items_y = 8
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ def YB_jia_yang_tou_da(
|
|||||||
height=height,
|
height=height,
|
||||||
max_volume=max_volume,
|
max_volume=max_volume,
|
||||||
barcode=barcode,
|
barcode=barcode,
|
||||||
model="Solid_Stock",
|
model="YB_jia_yang_tou_da_1X1_carrier",
|
||||||
)
|
)
|
||||||
|
|
||||||
"""液1x1"""
|
"""液1x1"""
|
||||||
@@ -51,7 +51,7 @@ def YB_ye_100ml_Bottle(
|
|||||||
height=height,
|
height=height,
|
||||||
max_volume=max_volume,
|
max_volume=max_volume,
|
||||||
barcode=barcode,
|
barcode=barcode,
|
||||||
model="Liquid_Bottle_100ml",
|
model="YB_1Bottle100mlCarrier",
|
||||||
)
|
)
|
||||||
|
|
||||||
"""高粘液"""
|
"""高粘液"""
|
||||||
@@ -87,7 +87,7 @@ def YB_fen_ye_5ml_Bottle(
|
|||||||
height=height,
|
height=height,
|
||||||
max_volume=max_volume,
|
max_volume=max_volume,
|
||||||
barcode=barcode,
|
barcode=barcode,
|
||||||
model="Separation_Bottle_5ml",
|
model="YB_fen_ye_5ml_Bottle",
|
||||||
)
|
)
|
||||||
|
|
||||||
"""20ml分液瓶"""
|
"""20ml分液瓶"""
|
||||||
@@ -105,7 +105,7 @@ def YB_fen_ye_20ml_Bottle(
|
|||||||
height=height,
|
height=height,
|
||||||
max_volume=max_volume,
|
max_volume=max_volume,
|
||||||
barcode=barcode,
|
barcode=barcode,
|
||||||
model="Separation_Bottle_20ml",
|
model="YB_fen_ye_20ml_Bottle",
|
||||||
)
|
)
|
||||||
|
|
||||||
"""配液瓶(小)"""
|
"""配液瓶(小)"""
|
||||||
@@ -123,7 +123,7 @@ def YB_pei_ye_xiao_Bottle(
|
|||||||
height=height,
|
height=height,
|
||||||
max_volume=max_volume,
|
max_volume=max_volume,
|
||||||
barcode=barcode,
|
barcode=barcode,
|
||||||
model="Mixing_Bottle_Small",
|
model="YB_pei_ye_xiao_Bottle",
|
||||||
)
|
)
|
||||||
|
|
||||||
"""配液瓶(大)"""
|
"""配液瓶(大)"""
|
||||||
@@ -141,7 +141,7 @@ def YB_pei_ye_da_Bottle(
|
|||||||
height=height,
|
height=height,
|
||||||
max_volume=max_volume,
|
max_volume=max_volume,
|
||||||
barcode=barcode,
|
barcode=barcode,
|
||||||
model="Mixing_Bottle_Large",
|
model="YB_pei_ye_da_Bottle",
|
||||||
)
|
)
|
||||||
|
|
||||||
"""枪头"""
|
"""枪头"""
|
||||||
@@ -159,5 +159,5 @@ def YB_Pipette_Tip(
|
|||||||
height=height,
|
height=height,
|
||||||
max_volume=max_volume,
|
max_volume=max_volume,
|
||||||
barcode=barcode,
|
barcode=barcode,
|
||||||
model="Pipette_Tip",
|
model="YB_Pipette_Tip",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ from typing import Optional, Dict, Any, List
|
|||||||
import rclpy
|
import rclpy
|
||||||
from unilabos_msgs.srv._serial_command import SerialCommand_Response
|
from unilabos_msgs.srv._serial_command import SerialCommand_Response
|
||||||
|
|
||||||
|
from unilabos.app.register import register_devices_and_resources
|
||||||
from unilabos.ros.nodes.presets.resource_mesh_manager import ResourceMeshManager
|
from unilabos.ros.nodes.presets.resource_mesh_manager import ResourceMeshManager
|
||||||
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker, ResourceTreeSet
|
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker, ResourceTreeSet
|
||||||
from unilabos.devices.ros_dev.liquid_handler_joint_publisher import LiquidHandlerJointPublisher
|
from unilabos.devices.ros_dev.liquid_handler_joint_publisher import LiquidHandlerJointPublisher
|
||||||
from unilabos_msgs.srv import SerialCommand # type: ignore
|
from unilabos_msgs.srv import SerialCommand # type: ignore
|
||||||
from rclpy.executors import MultiThreadedExecutor, SingleThreadedExecutor
|
from rclpy.executors import MultiThreadedExecutor
|
||||||
from rclpy.node import Node
|
from rclpy.node import Node
|
||||||
from rclpy.timer import Timer
|
from rclpy.timer import Timer
|
||||||
|
|
||||||
@@ -108,66 +109,51 @@ def slave(
|
|||||||
rclpy_init_args: List[str] = ["--log-level", "debug"],
|
rclpy_init_args: List[str] = ["--log-level", "debug"],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""从节点函数"""
|
"""从节点函数"""
|
||||||
|
# 1. 初始化 ROS2
|
||||||
if not rclpy.ok():
|
if not rclpy.ok():
|
||||||
rclpy.init(args=rclpy_init_args)
|
rclpy.init(args=rclpy_init_args)
|
||||||
executor = rclpy.__executor
|
executor = rclpy.__executor
|
||||||
if not executor:
|
if not executor:
|
||||||
executor = rclpy.__executor = MultiThreadedExecutor()
|
executor = rclpy.__executor = MultiThreadedExecutor()
|
||||||
devices_instances = {}
|
|
||||||
for device_config in devices_config.root_nodes:
|
|
||||||
device_id = device_config.res_content.id
|
|
||||||
if device_config.res_content.type != "device":
|
|
||||||
d = initialize_device_from_dict(device_id, device_config.get_nested_dict())
|
|
||||||
devices_instances[device_id] = d
|
|
||||||
# 默认初始化
|
|
||||||
# if d is not None and isinstance(d, Node):
|
|
||||||
# executor.add_node(d)
|
|
||||||
# else:
|
|
||||||
# print(f"Warning: Device {device_id} could not be initialized or is not a valid Node")
|
|
||||||
|
|
||||||
n = Node(f"slaveMachine_{BasicConfig.machine_name}", parameter_overrides=[])
|
# 1.5 启动 executor 线程
|
||||||
executor.add_node(n)
|
|
||||||
|
|
||||||
if visual != "disable":
|
|
||||||
from unilabos.ros.nodes.presets.joint_republisher import JointRepublisher
|
|
||||||
|
|
||||||
resource_mesh_manager = ResourceMeshManager(
|
|
||||||
resources_mesh_config,
|
|
||||||
resources_config, # type: ignore FIXME
|
|
||||||
resource_tracker=DeviceNodeResourceTracker(),
|
|
||||||
device_id="resource_mesh_manager",
|
|
||||||
)
|
|
||||||
joint_republisher = JointRepublisher("joint_republisher", DeviceNodeResourceTracker())
|
|
||||||
|
|
||||||
executor.add_node(resource_mesh_manager)
|
|
||||||
executor.add_node(joint_republisher)
|
|
||||||
thread = threading.Thread(target=executor.spin, daemon=True, name="slave_executor_thread")
|
thread = threading.Thread(target=executor.spin, daemon=True, name="slave_executor_thread")
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
|
# 2. 创建 Slave Machine Node
|
||||||
|
n = Node(f"slaveMachine_{BasicConfig.machine_name}", parameter_overrides=[])
|
||||||
|
executor.add_node(n)
|
||||||
|
|
||||||
|
# 3. 向 Host 报送节点信息和物料,获取 UUID 映射
|
||||||
|
uuid_mapping = {}
|
||||||
if not BasicConfig.slave_no_host:
|
if not BasicConfig.slave_no_host:
|
||||||
|
# 3.1 报送节点信息
|
||||||
sclient = n.create_client(SerialCommand, "/node_info_update")
|
sclient = n.create_client(SerialCommand, "/node_info_update")
|
||||||
sclient.wait_for_service()
|
sclient.wait_for_service()
|
||||||
|
|
||||||
|
registry_config = {}
|
||||||
|
devices_to_register, resources_to_register = register_devices_and_resources(lab_registry, True)
|
||||||
|
registry_config.update(devices_to_register)
|
||||||
|
registry_config.update(resources_to_register)
|
||||||
request = SerialCommand.Request()
|
request = SerialCommand.Request()
|
||||||
request.command = json.dumps(
|
request.command = json.dumps(
|
||||||
{
|
{
|
||||||
"machine_name": BasicConfig.machine_name,
|
"machine_name": BasicConfig.machine_name,
|
||||||
"type": "slave",
|
"type": "slave",
|
||||||
"devices_config": devices_config.dump(),
|
"devices_config": devices_config.dump(),
|
||||||
"registry_config": lab_registry.obtain_registry_device_info(),
|
"registry_config": registry_config,
|
||||||
},
|
},
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
cls=TypeEncoder,
|
cls=TypeEncoder,
|
||||||
)
|
)
|
||||||
response = sclient.call_async(request).result()
|
sclient.call_async(request).result()
|
||||||
logger.info(f"Slave node info updated.")
|
logger.info(f"Slave node info updated.")
|
||||||
|
|
||||||
# 使用新的 c2s_update_resource_tree 服务
|
# 3.2 报送物料树,获取 UUID 映射
|
||||||
rclient = n.create_client(SerialCommand, "/c2s_update_resource_tree")
|
|
||||||
rclient.wait_for_service()
|
|
||||||
|
|
||||||
# 序列化 ResourceTreeSet 为 JSON
|
|
||||||
if resources_config:
|
if resources_config:
|
||||||
|
rclient = n.create_client(SerialCommand, "/c2s_update_resource_tree")
|
||||||
|
rclient.wait_for_service()
|
||||||
|
|
||||||
request = SerialCommand.Request()
|
request = SerialCommand.Request()
|
||||||
request.command = json.dumps(
|
request.command = json.dumps(
|
||||||
{
|
{
|
||||||
@@ -180,35 +166,61 @@ def slave(
|
|||||||
},
|
},
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
)
|
)
|
||||||
tree_response: SerialCommand_Response = rclient.call_async(request).result()
|
tree_response: SerialCommand_Response = rclient.call(request)
|
||||||
uuid_mapping = json.loads(tree_response.response)
|
uuid_mapping = json.loads(tree_response.response)
|
||||||
# 创建反向映射:new_uuid -> old_uuid
|
logger.info(f"Slave resource tree added. UUID mapping: {len(uuid_mapping)} nodes")
|
||||||
reverse_uuid_mapping = {new_uuid: old_uuid for old_uuid, new_uuid in uuid_mapping.items()}
|
|
||||||
for node in resources_config.root_nodes:
|
# 3.3 使用 UUID 映射更新 resources_config 的 UUID(参考 client.py 逻辑)
|
||||||
if node.res_content.type == "device":
|
old_uuids = {node.res_content.uuid: node for node in resources_config.all_nodes}
|
||||||
for sub_node in node.children:
|
for old_uuid, node in old_uuids.items():
|
||||||
# 只有二级子设备
|
if old_uuid in uuid_mapping:
|
||||||
if sub_node.res_content.type != "device":
|
new_uuid = uuid_mapping[old_uuid]
|
||||||
device_tracker = devices_instances[node.res_content.id].resource_tracker
|
node.res_content.uuid = new_uuid
|
||||||
# sub_node.res_content.uuid 已经是新UUID,需要用旧UUID去查找
|
# 更新所有子节点的 parent_uuid
|
||||||
old_uuid = reverse_uuid_mapping.get(sub_node.res_content.uuid)
|
for child in node.children:
|
||||||
if old_uuid:
|
child.res_content.parent_uuid = new_uuid
|
||||||
# 找到旧UUID,使用UUID查找
|
|
||||||
resource_instance = device_tracker.figure_resource({"uuid": old_uuid})
|
|
||||||
else:
|
|
||||||
# 未找到旧UUID,使用name查找
|
|
||||||
resource_instance = device_tracker.figure_resource({"name": sub_node.res_content.name})
|
|
||||||
device_tracker.loop_update_uuid(resource_instance, uuid_mapping)
|
|
||||||
else:
|
else:
|
||||||
logger.error("Slave模式不允许新增非设备节点下的物料")
|
logger.warning(f"资源UUID未更新: {old_uuid}")
|
||||||
continue
|
|
||||||
if tree_response:
|
|
||||||
logger.info(f"Slave resource tree added. Response: {tree_response.response}")
|
|
||||||
else:
|
|
||||||
logger.warning("Slave resource tree add response is None")
|
|
||||||
else:
|
else:
|
||||||
logger.info("No resources to add.")
|
logger.info("No resources to add.")
|
||||||
|
|
||||||
|
# 4. 初始化所有设备实例(此时 resources_config 的 UUID 已更新)
|
||||||
|
devices_instances = {}
|
||||||
|
for device_config in devices_config.root_nodes:
|
||||||
|
device_id = device_config.res_content.id
|
||||||
|
if device_config.res_content.type == "device":
|
||||||
|
d = initialize_device_from_dict(device_id, device_config.get_nested_dict())
|
||||||
|
if d is not None:
|
||||||
|
devices_instances[device_id] = d
|
||||||
|
logger.info(f"Device {device_id} initialized.")
|
||||||
|
else:
|
||||||
|
logger.warning(f"Device {device_id} initialization failed.")
|
||||||
|
|
||||||
|
# 5. 如果启用可视化,创建可视化相关节点
|
||||||
|
if visual != "disable":
|
||||||
|
from unilabos.ros.nodes.presets.joint_republisher import JointRepublisher
|
||||||
|
|
||||||
|
# 将 ResourceTreeSet 转换为 list 用于 visual 组件
|
||||||
|
resources_list = (
|
||||||
|
[node.res_content.model_dump(by_alias=True) for node in resources_config.all_nodes]
|
||||||
|
if resources_config
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
resource_mesh_manager = ResourceMeshManager(
|
||||||
|
resources_mesh_config,
|
||||||
|
resources_list,
|
||||||
|
resource_tracker=DeviceNodeResourceTracker(),
|
||||||
|
device_id="resource_mesh_manager",
|
||||||
|
)
|
||||||
|
joint_republisher = JointRepublisher("joint_republisher", DeviceNodeResourceTracker())
|
||||||
|
lh_joint_pub = LiquidHandlerJointPublisher(
|
||||||
|
resources_config=resources_list, resource_tracker=DeviceNodeResourceTracker()
|
||||||
|
)
|
||||||
|
executor.add_node(resource_mesh_manager)
|
||||||
|
executor.add_node(joint_republisher)
|
||||||
|
executor.add_node(lh_joint_pub)
|
||||||
|
|
||||||
|
# 7. 保持运行
|
||||||
while True:
|
while True:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ from unilabos.ros.nodes.resource_tracker import (
|
|||||||
)
|
)
|
||||||
from unilabos.ros.x.rclpyx import get_event_loop
|
from unilabos.ros.x.rclpyx import get_event_loop
|
||||||
from unilabos.ros.utils.driver_creator import WorkstationNodeCreator, PyLabRobotCreator, DeviceClassCreator
|
from unilabos.ros.utils.driver_creator import WorkstationNodeCreator, PyLabRobotCreator, DeviceClassCreator
|
||||||
from unilabos.utils.async_util import run_async_func
|
from rclpy.task import Task, Future
|
||||||
from unilabos.utils.import_manager import default_manager
|
from unilabos.utils.import_manager import default_manager
|
||||||
from unilabos.utils.log import info, debug, warning, error, critical, logger, trace
|
from unilabos.utils.log import info, debug, warning, error, critical, logger, trace
|
||||||
from unilabos.utils.type_check import get_type_class, TypeEncoder, get_result_info_str
|
from unilabos.utils.type_check import get_type_class, TypeEncoder, get_result_info_str
|
||||||
@@ -555,6 +555,15 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
rclpy.get_global_executor().add_node(self)
|
rclpy.get_global_executor().add_node(self)
|
||||||
self.lab_logger().debug(f"ROS节点初始化完成")
|
self.lab_logger().debug(f"ROS节点初始化完成")
|
||||||
|
|
||||||
|
async def sleep(self, rel_time: float, callback_group=None):
|
||||||
|
if callback_group is None:
|
||||||
|
callback_group = self.callback_group
|
||||||
|
await ROS2DeviceNode.async_wait_for(self, rel_time, callback_group)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def create_task(cls, func, trace_error=True, **kwargs) -> Task:
|
||||||
|
return ROS2DeviceNode.run_async_func(func, trace_error, **kwargs)
|
||||||
|
|
||||||
async def update_resource(self, resources: List["ResourcePLR"]):
|
async def update_resource(self, resources: List["ResourcePLR"]):
|
||||||
r = SerialCommand.Request()
|
r = SerialCommand.Request()
|
||||||
tree_set = ResourceTreeSet.from_plr_resources(resources)
|
tree_set = ResourceTreeSet.from_plr_resources(resources)
|
||||||
@@ -629,6 +638,145 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
- remove: 从资源树中移除资源
|
- remove: 从资源树中移除资源
|
||||||
"""
|
"""
|
||||||
from pylabrobot.resources.resource import Resource as ResourcePLR
|
from pylabrobot.resources.resource import Resource as ResourcePLR
|
||||||
|
|
||||||
|
def _handle_add(
|
||||||
|
plr_resources: List[ResourcePLR], tree_set: ResourceTreeSet, additional_add_params: Dict[str, Any]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
处理资源添加操作的内部函数
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plr_resources: PLR资源列表
|
||||||
|
tree_set: 资源树集合
|
||||||
|
additional_add_params: 额外的添加参数
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
操作结果字典
|
||||||
|
"""
|
||||||
|
for plr_resource, tree in zip(plr_resources, tree_set.trees):
|
||||||
|
self.resource_tracker.add_resource(plr_resource)
|
||||||
|
self.transfer_to_new_resource(plr_resource, tree, additional_add_params)
|
||||||
|
|
||||||
|
func = getattr(self.driver_instance, "resource_tree_add", None)
|
||||||
|
if callable(func):
|
||||||
|
func(plr_resources)
|
||||||
|
|
||||||
|
return {"success": True, "action": "add"}
|
||||||
|
|
||||||
|
def _handle_remove(resources_uuid: List[str]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
处理资源移除操作的内部函数
|
||||||
|
|
||||||
|
Args:
|
||||||
|
resources_uuid: 要移除的资源UUID列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
操作结果字典,包含移除的资源列表
|
||||||
|
"""
|
||||||
|
found_resources: List[List[Union[ResourcePLR, dict]]] = self.resource_tracker.figure_resource(
|
||||||
|
[{"uuid": uid} for uid in resources_uuid], try_mode=True
|
||||||
|
)
|
||||||
|
found_plr_resources = []
|
||||||
|
other_plr_resources = []
|
||||||
|
|
||||||
|
for found_resource in found_resources:
|
||||||
|
for resource in found_resource:
|
||||||
|
if issubclass(resource.__class__, ResourcePLR):
|
||||||
|
found_plr_resources.append(resource)
|
||||||
|
else:
|
||||||
|
other_plr_resources.append(resource)
|
||||||
|
|
||||||
|
# 调用driver的remove回调
|
||||||
|
func = getattr(self.driver_instance, "resource_tree_remove", None)
|
||||||
|
if callable(func):
|
||||||
|
func(found_plr_resources)
|
||||||
|
|
||||||
|
# 从parent卸载并从tracker移除
|
||||||
|
for plr_resource in found_plr_resources:
|
||||||
|
if plr_resource.parent is not None:
|
||||||
|
plr_resource.parent.unassign_child_resource(plr_resource)
|
||||||
|
self.resource_tracker.remove_resource(plr_resource)
|
||||||
|
self.lab_logger().info(f"移除物料 {plr_resource} 及其子节点")
|
||||||
|
|
||||||
|
for other_plr_resource in other_plr_resources:
|
||||||
|
self.resource_tracker.remove_resource(other_plr_resource)
|
||||||
|
self.lab_logger().info(f"移除物料 {other_plr_resource} 及其子节点")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"action": "remove",
|
||||||
|
# "removed_plr": found_plr_resources,
|
||||||
|
# "removed_other": other_plr_resources,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _handle_update(
|
||||||
|
plr_resources: List[ResourcePLR], tree_set: ResourceTreeSet, additional_add_params: Dict[str, Any]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
处理资源更新操作的内部函数
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plr_resources: PLR资源列表(包含新状态)
|
||||||
|
tree_set: 资源树集合
|
||||||
|
additional_add_params: 额外的参数
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
操作结果字典
|
||||||
|
"""
|
||||||
|
for plr_resource, tree in zip(plr_resources, tree_set.trees):
|
||||||
|
states = plr_resource.serialize_all_state()
|
||||||
|
original_instance: ResourcePLR = self.resource_tracker.figure_resource(
|
||||||
|
{"uuid": tree.root_node.res_content.uuid}, try_mode=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update操作中包含改名:需要先remove再add
|
||||||
|
if original_instance.name != plr_resource.name:
|
||||||
|
old_name = original_instance.name
|
||||||
|
new_name = plr_resource.name
|
||||||
|
self.lab_logger().info(f"物料改名操作:{old_name} -> {new_name}")
|
||||||
|
|
||||||
|
# 收集所有相关的uuid(包括子节点)
|
||||||
|
_handle_remove([original_instance.unilabos_uuid])
|
||||||
|
original_instance.name = new_name
|
||||||
|
_handle_add([original_instance], tree_set, additional_add_params)
|
||||||
|
|
||||||
|
self.lab_logger().info(f"物料改名完成:{old_name} -> {new_name}")
|
||||||
|
|
||||||
|
# 常规更新:不涉及改名
|
||||||
|
original_parent_resource = original_instance.parent
|
||||||
|
original_parent_resource_uuid = getattr(original_parent_resource, "unilabos_uuid", None)
|
||||||
|
target_parent_resource_uuid = tree.root_node.res_content.uuid_parent
|
||||||
|
|
||||||
|
self.lab_logger().info(
|
||||||
|
f"物料{original_instance} 原始父节点{original_parent_resource_uuid} "
|
||||||
|
f"目标父节点{target_parent_resource_uuid} 更新"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 更新extra
|
||||||
|
if getattr(plr_resource, "unilabos_extra", None) is not None:
|
||||||
|
original_instance.unilabos_extra = getattr(plr_resource, "unilabos_extra") # type: ignore # noqa: E501
|
||||||
|
|
||||||
|
# 如果父节点变化,需要重新挂载
|
||||||
|
if (
|
||||||
|
original_parent_resource_uuid != target_parent_resource_uuid
|
||||||
|
and original_parent_resource is not None
|
||||||
|
):
|
||||||
|
self.transfer_to_new_resource(original_instance, tree, additional_add_params)
|
||||||
|
|
||||||
|
# 加载状态
|
||||||
|
original_instance.load_all_state(states)
|
||||||
|
child_count = len(original_instance.get_all_children())
|
||||||
|
self.lab_logger().info(
|
||||||
|
f"更新了资源属性 {plr_resource}[{tree.root_node.res_content.uuid}] " f"及其子节点 {child_count} 个"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 调用driver的update回调
|
||||||
|
func = getattr(self.driver_instance, "resource_tree_update", None)
|
||||||
|
if callable(func):
|
||||||
|
func(plr_resources)
|
||||||
|
|
||||||
|
return {"success": True, "action": "update"}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = json.loads(req.command)
|
data = json.loads(req.command)
|
||||||
results = []
|
results = []
|
||||||
@@ -647,7 +795,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
].call_async(
|
].call_async(
|
||||||
SerialCommand.Request(
|
SerialCommand.Request(
|
||||||
command=json.dumps(
|
command=json.dumps(
|
||||||
{"data": {"data": resources_uuid, "with_children": False}, "action": "get"}
|
{"data": {"data": resources_uuid, "with_children": True if action == "add" else False}, "action": "get"}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
@@ -655,68 +803,20 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
tree_set = ResourceTreeSet.from_raw_list(raw_nodes)
|
tree_set = ResourceTreeSet.from_raw_list(raw_nodes)
|
||||||
try:
|
try:
|
||||||
if action == "add":
|
if action == "add":
|
||||||
# 添加资源到资源跟踪器
|
if tree_set is None:
|
||||||
|
raise ValueError("tree_set不能为None")
|
||||||
plr_resources = tree_set.to_plr_resources()
|
plr_resources = tree_set.to_plr_resources()
|
||||||
for plr_resource, tree in zip(plr_resources, tree_set.trees):
|
result = _handle_add(plr_resources, tree_set, additional_add_params)
|
||||||
self.resource_tracker.add_resource(plr_resource)
|
results.append(result)
|
||||||
self.transfer_to_new_resource(plr_resource, tree, additional_add_params)
|
|
||||||
func = getattr(self.driver_instance, "resource_tree_add", None)
|
|
||||||
if callable(func):
|
|
||||||
func(plr_resources)
|
|
||||||
results.append({"success": True, "action": "add"})
|
|
||||||
elif action == "update":
|
elif action == "update":
|
||||||
# 更新资源
|
if tree_set is None:
|
||||||
|
raise ValueError("tree_set不能为None")
|
||||||
plr_resources = tree_set.to_plr_resources()
|
plr_resources = tree_set.to_plr_resources()
|
||||||
for plr_resource, tree in zip(plr_resources, tree_set.trees):
|
result = _handle_update(plr_resources, tree_set, additional_add_params)
|
||||||
states = plr_resource.serialize_all_state()
|
results.append(result)
|
||||||
original_instance: ResourcePLR = self.resource_tracker.figure_resource(
|
|
||||||
{"uuid": tree.root_node.res_content.uuid}, try_mode=False
|
|
||||||
)
|
|
||||||
original_parent_resource = original_instance.parent
|
|
||||||
original_parent_resource_uuid = getattr(original_parent_resource, "unilabos_uuid", None)
|
|
||||||
target_parent_resource_uuid = tree.root_node.res_content.uuid_parent
|
|
||||||
self.lab_logger().info(
|
|
||||||
f"物料{original_instance} 原始父节点{original_parent_resource_uuid} 目标父节点{target_parent_resource_uuid} 更新"
|
|
||||||
)
|
|
||||||
# todo: 对extra进行update
|
|
||||||
if getattr(plr_resource, "unilabos_extra", None) is not None:
|
|
||||||
original_instance.unilabos_extra = getattr(plr_resource, "unilabos_extra")
|
|
||||||
if original_parent_resource_uuid != target_parent_resource_uuid and original_parent_resource is not None:
|
|
||||||
self.transfer_to_new_resource(original_instance, tree, additional_add_params)
|
|
||||||
original_instance.load_all_state(states)
|
|
||||||
self.lab_logger().info(
|
|
||||||
f"更新了资源属性 {plr_resource}[{tree.root_node.res_content.uuid}] 及其子节点 {len(original_instance.get_all_children())} 个"
|
|
||||||
)
|
|
||||||
|
|
||||||
func = getattr(self.driver_instance, "resource_tree_update", None)
|
|
||||||
if callable(func):
|
|
||||||
func(plr_resources)
|
|
||||||
results.append({"success": True, "action": "update"})
|
|
||||||
elif action == "remove":
|
elif action == "remove":
|
||||||
# 移除资源
|
result = _handle_remove(resources_uuid)
|
||||||
found_resources: List[List[Union[ResourcePLR, dict]]] = self.resource_tracker.figure_resource(
|
results.append(result)
|
||||||
[{"uuid": uid} for uid in resources_uuid], try_mode=True
|
|
||||||
)
|
|
||||||
found_plr_resources = []
|
|
||||||
other_plr_resources = []
|
|
||||||
for found_resource in found_resources:
|
|
||||||
for resource in found_resource:
|
|
||||||
if issubclass(resource.__class__, ResourcePLR):
|
|
||||||
found_plr_resources.append(resource)
|
|
||||||
else:
|
|
||||||
other_plr_resources.append(resource)
|
|
||||||
func = getattr(self.driver_instance, "resource_tree_remove", None)
|
|
||||||
if callable(func):
|
|
||||||
func(found_plr_resources)
|
|
||||||
for plr_resource in found_plr_resources:
|
|
||||||
if plr_resource.parent is not None:
|
|
||||||
plr_resource.parent.unassign_child_resource(plr_resource)
|
|
||||||
self.resource_tracker.remove_resource(plr_resource)
|
|
||||||
self.lab_logger().info(f"移除物料 {plr_resource} 及其子节点")
|
|
||||||
for other_plr_resource in other_plr_resources:
|
|
||||||
self.resource_tracker.remove_resource(other_plr_resource)
|
|
||||||
self.lab_logger().info(f"移除物料 {other_plr_resource} 及其子节点")
|
|
||||||
results.append({"success": True, "action": "remove"})
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"Error processing {action} operation: {str(e)}"
|
error_msg = f"Error processing {action} operation: {str(e)}"
|
||||||
self.lab_logger().error(f"[Resource Tree Update] {error_msg}")
|
self.lab_logger().error(f"[Resource Tree Update] {error_msg}")
|
||||||
@@ -725,7 +825,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
|
|
||||||
# 返回处理结果
|
# 返回处理结果
|
||||||
result_json = {"results": results, "total": len(data)}
|
result_json = {"results": results, "total": len(data)}
|
||||||
res.response = json.dumps(result_json, ensure_ascii=False)
|
res.response = json.dumps(result_json, ensure_ascii=False, cls=TypeEncoder)
|
||||||
self.lab_logger().info(f"[Resource Tree Update] Completed processing {len(data)} operations")
|
self.lab_logger().info(f"[Resource Tree Update] Completed processing {len(data)} operations")
|
||||||
|
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
@@ -995,9 +1095,14 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
|
|
||||||
# 通过资源跟踪器获取本地实例
|
# 通过资源跟踪器获取本地实例
|
||||||
final_resources = queried_resources if is_sequence else queried_resources[0]
|
final_resources = queried_resources if is_sequence else queried_resources[0]
|
||||||
final_resources = self.resource_tracker.figure_resource({"name": final_resources.name}, try_mode=False) if not is_sequence else [
|
final_resources = (
|
||||||
self.resource_tracker.figure_resource({"name": res.name}, try_mode=False) for res in queried_resources
|
self.resource_tracker.figure_resource({"name": final_resources.name}, try_mode=False)
|
||||||
]
|
if not is_sequence
|
||||||
|
else [
|
||||||
|
self.resource_tracker.figure_resource({"name": res.name}, try_mode=False)
|
||||||
|
for res in queried_resources
|
||||||
|
]
|
||||||
|
)
|
||||||
action_kwargs[k] = final_resources
|
action_kwargs[k] = final_resources
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1218,6 +1323,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
raise JsonCommandInitError(
|
raise JsonCommandInitError(
|
||||||
f"执行动作时JSON缺少function_name或function_args: {ex}\n原JSON: {string}\n{traceback.format_exc()}"
|
f"执行动作时JSON缺少function_name或function_args: {ex}\n原JSON: {string}\n{traceback.format_exc()}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _convert_resource_sync(self, resource_data: Dict[str, Any]):
|
def _convert_resource_sync(self, resource_data: Dict[str, Any]):
|
||||||
"""同步转换资源数据为实例"""
|
"""同步转换资源数据为实例"""
|
||||||
# 创建资源查询请求
|
# 创建资源查询请求
|
||||||
@@ -1385,18 +1491,27 @@ class ROS2DeviceNode:
|
|||||||
它不继承设备类,而是通过代理模式访问设备类的属性和方法。
|
它不继承设备类,而是通过代理模式访问设备类的属性和方法。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 类变量,用于循环管理
|
@classmethod
|
||||||
_loop = None
|
def run_async_func(cls, func, trace_error=True, **kwargs) -> Task:
|
||||||
_loop_running = False
|
def _handle_future_exception(fut):
|
||||||
_loop_thread = None
|
try:
|
||||||
|
fut.result()
|
||||||
|
except Exception as e:
|
||||||
|
error(f"异步任务 {func.__name__} 报错了")
|
||||||
|
error(traceback.format_exc())
|
||||||
|
|
||||||
|
future = rclpy.get_global_executor().create_task(func(**kwargs))
|
||||||
|
if trace_error:
|
||||||
|
future.add_done_callback(_handle_future_exception)
|
||||||
|
return future
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_loop(cls):
|
async def async_wait_for(cls, node: Node, wait_time: float, callback_group=None):
|
||||||
return cls._loop
|
future = Future()
|
||||||
|
timer = node.create_timer(wait_time, lambda : future.set_result(None), callback_group=callback_group, clock=node.get_clock())
|
||||||
@classmethod
|
await future
|
||||||
def run_async_func(cls, func, trace_error=True, **kwargs):
|
timer.cancel()
|
||||||
return run_async_func(func, loop=cls._loop, trace_error=trace_error, **kwargs)
|
node.destroy_timer(timer)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def driver_instance(self):
|
def driver_instance(self):
|
||||||
@@ -1436,11 +1551,6 @@ class ROS2DeviceNode:
|
|||||||
print_publish: 是否打印发布信息
|
print_publish: 是否打印发布信息
|
||||||
driver_is_ros:
|
driver_is_ros:
|
||||||
"""
|
"""
|
||||||
# 在初始化时检查循环状态
|
|
||||||
if ROS2DeviceNode._loop_running and ROS2DeviceNode._loop_thread is not None:
|
|
||||||
pass
|
|
||||||
elif ROS2DeviceNode._loop_thread is None:
|
|
||||||
self._start_loop()
|
|
||||||
|
|
||||||
# 保存设备类是否支持异步上下文
|
# 保存设备类是否支持异步上下文
|
||||||
self._has_async_context = hasattr(driver_class, "__aenter__") and hasattr(driver_class, "__aexit__")
|
self._has_async_context = hasattr(driver_class, "__aenter__") and hasattr(driver_class, "__aexit__")
|
||||||
@@ -1529,17 +1639,6 @@ class ROS2DeviceNode:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._ros_node.lab_logger().error(f"设备后初始化失败: {e}")
|
self._ros_node.lab_logger().error(f"设备后初始化失败: {e}")
|
||||||
|
|
||||||
def _start_loop(self):
|
|
||||||
def run_event_loop():
|
|
||||||
loop = asyncio.new_event_loop()
|
|
||||||
ROS2DeviceNode._loop = loop
|
|
||||||
asyncio.set_event_loop(loop)
|
|
||||||
loop.run_forever()
|
|
||||||
|
|
||||||
ROS2DeviceNode._loop_thread = threading.Thread(target=run_event_loop, daemon=True, name="ROS2DeviceNodeLoop")
|
|
||||||
ROS2DeviceNode._loop_thread.start()
|
|
||||||
logger.info(f"循环线程已启动")
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceInfoType(TypedDict):
|
class DeviceInfoType(TypedDict):
|
||||||
id: str
|
id: str
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ from unilabos_msgs.srv import (
|
|||||||
ResourceDelete,
|
ResourceDelete,
|
||||||
ResourceUpdate,
|
ResourceUpdate,
|
||||||
ResourceList,
|
ResourceList,
|
||||||
SerialCommand, ResourceGet,
|
SerialCommand,
|
||||||
|
ResourceGet,
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
|
from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
|
||||||
from unique_identifier_msgs.msg import UUID
|
from unique_identifier_msgs.msg import UUID
|
||||||
@@ -164,29 +165,16 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
# resources_config 的 root node 是
|
# resources_config 的 root node 是
|
||||||
# # 创建反向映射:new_uuid -> old_uuid
|
# # 创建反向映射:new_uuid -> old_uuid
|
||||||
# reverse_uuid_mapping = {new_uuid: old_uuid for old_uuid, new_uuid in uuid_mapping.items()}
|
# reverse_uuid_mapping = {new_uuid: old_uuid for old_uuid, new_uuid in uuid_mapping.items()}
|
||||||
# for tree in resources_config.trees:
|
for tree in resources_config.trees:
|
||||||
# node = tree.root_node
|
node = tree.root_node
|
||||||
# if node.res_content.type == "device":
|
if node.res_content.type == "device":
|
||||||
# if node.res_content.id == "host_node":
|
continue
|
||||||
# continue
|
else:
|
||||||
# # slave节点走c2s更新接口,拿到add自行update uuid
|
try:
|
||||||
# device_tracker = self.devices_instances[node.res_content.id].resource_tracker
|
for plr_resource in ResourceTreeSet([tree]).to_plr_resources():
|
||||||
# old_uuid = reverse_uuid_mapping.get(node.res_content.uuid)
|
self._resource_tracker.add_resource(plr_resource)
|
||||||
# if old_uuid:
|
except Exception as ex:
|
||||||
# # 找到旧UUID,使用UUID查找
|
self.lab_logger().warning(f"[Host Node-Resource] 根节点物料{tree}序列化失败!")
|
||||||
# resource_instance = device_tracker.uuid_to_resources.get(old_uuid)
|
|
||||||
# else:
|
|
||||||
# # 未找到旧UUID,使用name查找
|
|
||||||
# resource_instance = device_tracker.figure_resource(
|
|
||||||
# {"name": node.res_content.name}
|
|
||||||
# )
|
|
||||||
# device_tracker.loop_update_uuid(resource_instance, uuid_mapping)
|
|
||||||
# else:
|
|
||||||
# try:
|
|
||||||
# for plr_resource in ResourceTreeSet([tree]).to_plr_resources():
|
|
||||||
# self.resource_tracker.add_resource(plr_resource)
|
|
||||||
# except Exception as ex:
|
|
||||||
# self.lab_logger().warning("[Host Node-Resource] 根节点物料序列化失败!")
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.error(f"[Host Node-Resource] 添加物料出错!\n{traceback.format_exc()}")
|
logger.error(f"[Host Node-Resource] 添加物料出错!\n{traceback.format_exc()}")
|
||||||
# 初始化Node基类,传递空参数覆盖列表
|
# 初始化Node基类,传递空参数覆盖列表
|
||||||
@@ -877,11 +865,10 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
success = False
|
success = False
|
||||||
uuid_mapping = {}
|
uuid_mapping = {}
|
||||||
if len(self.bridges) > 0:
|
if len(self.bridges) > 0:
|
||||||
from unilabos.app.web.client import HTTPClient
|
from unilabos.app.web.client import HTTPClient, http_client
|
||||||
|
|
||||||
client: HTTPClient = self.bridges[-1]
|
|
||||||
resource_start_time = time.time()
|
resource_start_time = time.time()
|
||||||
uuid_mapping = client.resource_tree_add(resource_tree_set, mount_uuid, first_add)
|
uuid_mapping = http_client.resource_tree_add(resource_tree_set, mount_uuid, first_add)
|
||||||
success = True
|
success = True
|
||||||
resource_end_time = time.time()
|
resource_end_time = time.time()
|
||||||
self.lab_logger().info(
|
self.lab_logger().info(
|
||||||
@@ -989,9 +976,10 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
"""
|
"""
|
||||||
更新节点信息回调
|
更新节点信息回调
|
||||||
"""
|
"""
|
||||||
self.lab_logger().info(f"[Host Node] Node info update request received: {request}")
|
# self.lab_logger().info(f"[Host Node] Node info update request received: {request}")
|
||||||
try:
|
try:
|
||||||
from unilabos.app.communication import get_communication_client
|
from unilabos.app.communication import get_communication_client
|
||||||
|
from unilabos.app.web.client import HTTPClient, http_client
|
||||||
|
|
||||||
info = json.loads(request.command)
|
info = json.loads(request.command)
|
||||||
if "SYNC_SLAVE_NODE_INFO" in info:
|
if "SYNC_SLAVE_NODE_INFO" in info:
|
||||||
@@ -1000,10 +988,10 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
edge_device_id = info["edge_device_id"]
|
edge_device_id = info["edge_device_id"]
|
||||||
self.device_machine_names[edge_device_id] = machine_name
|
self.device_machine_names[edge_device_id] = machine_name
|
||||||
else:
|
else:
|
||||||
comm_client = get_communication_client()
|
devices_config = info.pop("devices_config")
|
||||||
registry_config = info["registry_config"]
|
registry_config = info.pop("registry_config")
|
||||||
for device_config in registry_config:
|
if registry_config:
|
||||||
comm_client.publish_registry(device_config["id"], device_config)
|
http_client.resource_registry({"resources": registry_config})
|
||||||
self.lab_logger().debug(f"[Host Node] Node info update: {info}")
|
self.lab_logger().debug(f"[Host Node] Node info update: {info}")
|
||||||
response.response = "OK"
|
response.response = "OK"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1029,10 +1017,9 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
|
|
||||||
success = False
|
success = False
|
||||||
if len(self.bridges) > 0: # 边的提交待定
|
if len(self.bridges) > 0: # 边的提交待定
|
||||||
from unilabos.app.web.client import HTTPClient
|
from unilabos.app.web.client import HTTPClient, http_client
|
||||||
|
|
||||||
client: HTTPClient = self.bridges[-1]
|
r = http_client.resource_add(add_schema(resources))
|
||||||
r = client.resource_add(add_schema(resources))
|
|
||||||
success = bool(r)
|
success = bool(r)
|
||||||
|
|
||||||
response.success = success
|
response.success = success
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import traceback
|
|||||||
import uuid
|
import uuid
|
||||||
from pydantic import BaseModel, field_serializer, field_validator
|
from pydantic import BaseModel, field_serializer, field_validator
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
from typing import List, Tuple, Any, Dict, Literal, Optional, cast, TYPE_CHECKING
|
from typing import List, Tuple, Any, Dict, Literal, Optional, cast, TYPE_CHECKING, Union
|
||||||
|
|
||||||
from unilabos.utils.log import logger
|
from unilabos.utils.log import logger
|
||||||
|
|
||||||
@@ -894,7 +894,7 @@ class DeviceNodeResourceTracker(object):
|
|||||||
new_uuid = name_to_uuid_map[resource_name]
|
new_uuid = name_to_uuid_map[resource_name]
|
||||||
self.set_resource_uuid(res, new_uuid)
|
self.set_resource_uuid(res, new_uuid)
|
||||||
self.uuid_to_resources[new_uuid] = res
|
self.uuid_to_resources[new_uuid] = res
|
||||||
logger.debug(f"设置资源UUID: {resource_name} -> {new_uuid}")
|
logger.trace(f"设置资源UUID: {resource_name} -> {new_uuid}")
|
||||||
return 1
|
return 1
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@@ -917,7 +917,8 @@ class DeviceNodeResourceTracker(object):
|
|||||||
if resource_name and resource_name in name_to_extra_map:
|
if resource_name and resource_name in name_to_extra_map:
|
||||||
extra = name_to_extra_map[resource_name]
|
extra = name_to_extra_map[resource_name]
|
||||||
self.set_resource_extra(res, extra)
|
self.set_resource_extra(res, extra)
|
||||||
logger.debug(f"设置资源Extra: {resource_name} -> {extra}")
|
if len(extra):
|
||||||
|
logger.debug(f"设置资源Extra: {resource_name} -> {extra}")
|
||||||
return 1
|
return 1
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@@ -927,7 +928,7 @@ class DeviceNodeResourceTracker(object):
|
|||||||
"""
|
"""
|
||||||
递归遍历资源树,更新所有节点的uuid
|
递归遍历资源树,更新所有节点的uuid
|
||||||
|
|
||||||
Args:0
|
Args:
|
||||||
resource: 资源对象(可以是dict或实例)
|
resource: 资源对象(可以是dict或实例)
|
||||||
uuid_map: uuid映射字典,{old_uuid: new_uuid}
|
uuid_map: uuid映射字典,{old_uuid: new_uuid}
|
||||||
|
|
||||||
@@ -952,6 +953,27 @@ class DeviceNodeResourceTracker(object):
|
|||||||
|
|
||||||
return self._traverse_and_process(resource, process)
|
return self._traverse_and_process(resource, process)
|
||||||
|
|
||||||
|
def loop_gather_uuid(self, resource) -> List[str]:
|
||||||
|
"""
|
||||||
|
递归遍历资源树,收集所有节点的uuid
|
||||||
|
|
||||||
|
Args:
|
||||||
|
resource: 资源对象(可以是dict或实例)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
收集到的uuid列表
|
||||||
|
"""
|
||||||
|
uuid_list = []
|
||||||
|
|
||||||
|
def process(res):
|
||||||
|
current_uuid = self._get_resource_attr(res, "uuid", "unilabos_uuid")
|
||||||
|
if current_uuid:
|
||||||
|
uuid_list.append(current_uuid)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
self._traverse_and_process(resource, process)
|
||||||
|
return uuid_list
|
||||||
|
|
||||||
def _collect_uuid_mapping(self, resource):
|
def _collect_uuid_mapping(self, resource):
|
||||||
"""
|
"""
|
||||||
递归收集资源的 uuid 映射到 uuid_to_resources
|
递归收集资源的 uuid 映射到 uuid_to_resources
|
||||||
@@ -965,14 +987,15 @@ class DeviceNodeResourceTracker(object):
|
|||||||
if current_uuid:
|
if current_uuid:
|
||||||
old = self.uuid_to_resources.get(current_uuid)
|
old = self.uuid_to_resources.get(current_uuid)
|
||||||
self.uuid_to_resources[current_uuid] = res
|
self.uuid_to_resources[current_uuid] = res
|
||||||
logger.debug(
|
logger.trace(
|
||||||
f"收集资源UUID映射: {current_uuid} -> {res} {'' if old is None else f'(覆盖旧值: {old})'}"
|
f"收集资源UUID映射: {current_uuid} -> {res} {'' if old is None else f'(覆盖旧值: {old})'}"
|
||||||
)
|
)
|
||||||
|
return 1
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
self._traverse_and_process(resource, process)
|
self._traverse_and_process(resource, process)
|
||||||
|
|
||||||
def _remove_uuid_mapping(self, resource):
|
def _remove_uuid_mapping(self, resource) -> int:
|
||||||
"""
|
"""
|
||||||
递归清除资源的 uuid 映射
|
递归清除资源的 uuid 映射
|
||||||
|
|
||||||
@@ -984,10 +1007,11 @@ class DeviceNodeResourceTracker(object):
|
|||||||
current_uuid = self._get_resource_attr(res, "uuid", "unilabos_uuid")
|
current_uuid = self._get_resource_attr(res, "uuid", "unilabos_uuid")
|
||||||
if current_uuid and current_uuid in self.uuid_to_resources:
|
if current_uuid and current_uuid in self.uuid_to_resources:
|
||||||
self.uuid_to_resources.pop(current_uuid)
|
self.uuid_to_resources.pop(current_uuid)
|
||||||
logger.debug(f"移除资源UUID映射: {current_uuid} -> {res}")
|
logger.trace(f"移除资源UUID映射: {current_uuid} -> {res}")
|
||||||
|
return 1
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
self._traverse_and_process(resource, process)
|
return self._traverse_and_process(resource, process)
|
||||||
|
|
||||||
def parent_resource(self, resource):
|
def parent_resource(self, resource):
|
||||||
if id(resource) in self.resource2parent_resource:
|
if id(resource) in self.resource2parent_resource:
|
||||||
@@ -1042,13 +1066,12 @@ class DeviceNodeResourceTracker(object):
|
|||||||
removed = True
|
removed = True
|
||||||
break
|
break
|
||||||
|
|
||||||
if not removed:
|
# 递归清除uuid映射
|
||||||
|
count = self._remove_uuid_mapping(resource)
|
||||||
|
if not count:
|
||||||
logger.warning(f"尝试移除不存在的资源: {resource}")
|
logger.warning(f"尝试移除不存在的资源: {resource}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 递归清除uuid映射
|
|
||||||
self._remove_uuid_mapping(resource)
|
|
||||||
|
|
||||||
# 清除 resource2parent_resource 中与该资源相关的映射
|
# 清除 resource2parent_resource 中与该资源相关的映射
|
||||||
# 需要清除:1) 该资源作为 key 的映射 2) 该资源作为 value 的映射
|
# 需要清除:1) 该资源作为 key 的映射 2) 该资源作为 value 的映射
|
||||||
keys_to_remove = []
|
keys_to_remove = []
|
||||||
@@ -1071,7 +1094,9 @@ class DeviceNodeResourceTracker(object):
|
|||||||
self.uuid_to_resources.clear()
|
self.uuid_to_resources.clear()
|
||||||
self.resource2parent_resource.clear()
|
self.resource2parent_resource.clear()
|
||||||
|
|
||||||
def figure_resource(self, query_resource, try_mode=False):
|
def figure_resource(
|
||||||
|
self, query_resource: Union[List[Union[dict, "PLRResource"]], dict, "PLRResource"], try_mode=False
|
||||||
|
) -> Union[List[Union[dict, "PLRResource", List[Union[dict, "PLRResource"]]]], dict, "PLRResource"]:
|
||||||
if isinstance(query_resource, list):
|
if isinstance(query_resource, list):
|
||||||
return [self.figure_resource(r, try_mode) for r in query_resource]
|
return [self.figure_resource(r, try_mode) for r in query_resource]
|
||||||
elif (
|
elif (
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import traceback
|
|
||||||
from asyncio import get_event_loop
|
|
||||||
|
|
||||||
from unilabos.utils.log import error
|
|
||||||
|
|
||||||
|
|
||||||
def run_async_func(func, *, loop=None, trace_error=True, **kwargs):
|
|
||||||
if loop is None:
|
|
||||||
loop = get_event_loop()
|
|
||||||
|
|
||||||
def _handle_future_exception(fut):
|
|
||||||
try:
|
|
||||||
fut.result()
|
|
||||||
except Exception as e:
|
|
||||||
error(f"异步任务 {func.__name__} 报错了")
|
|
||||||
error(traceback.format_exc())
|
|
||||||
|
|
||||||
future = asyncio.run_coroutine_threadsafe(func(**kwargs), loop)
|
|
||||||
if trace_error:
|
|
||||||
future.add_done_callback(_handle_future_exception)
|
|
||||||
return future
|
|
||||||
Reference in New Issue
Block a user