mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2026-02-06 06:25:06 +00:00
Adapt to new scheduler, sampels, and edge upload format (#230)
* add sample_material * adapt to new samples sys * fix pump transfer. fix resource update when protocol & ros callback * Adapt to new scheduler.
This commit is contained in:
@@ -690,16 +690,14 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
||||
)
|
||||
|
||||
def set_liquid_from_plate(
|
||||
self, plate: List[ResourceSlot], well_names: list[str], liquid_names: list[str], volumes: list[float]
|
||||
self, plate: ResourceSlot, well_names: list[str], liquid_names: list[str], volumes: list[float]
|
||||
) -> SetLiquidFromPlateReturn:
|
||||
"""Set the liquid in wells of a plate by well names (e.g., A1, A2, B3).
|
||||
|
||||
如果 liquid_names 和 volumes 为空,但 plate 和 well_names 不为空,直接返回 plate 和 wells。
|
||||
"""
|
||||
if isinstance(plate, list): # 未来移除
|
||||
plate = plate[0]
|
||||
assert issubclass(plate.__class__, Plate), "plate must be a Plate"
|
||||
plate: Plate = cast(Plate, plate)
|
||||
plate: Plate = cast(Plate, cast(Resource, plate))
|
||||
# 根据 well_names 获取对应的 Well 对象
|
||||
wells = [plate.get_well(name) for name in well_names]
|
||||
res_volumes = []
|
||||
|
||||
@@ -595,7 +595,7 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
||||
return super().set_liquid(wells, liquid_names, volumes)
|
||||
|
||||
def set_liquid_from_plate(
|
||||
self, plate: List[ResourceSlot], well_names: list[str], liquid_names: list[str], volumes: list[float]
|
||||
self, plate: ResourceSlot, well_names: list[str], liquid_names: list[str], volumes: list[float]
|
||||
) -> SetLiquidFromPlateReturn:
|
||||
return super().set_liquid_from_plate(plate, well_names, liquid_names, volumes)
|
||||
|
||||
|
||||
@@ -15,35 +15,35 @@ class VirtualPumpMode(Enum):
|
||||
|
||||
class VirtualTransferPump:
|
||||
"""虚拟转移泵类 - 模拟泵的基本功能,无需实际硬件 🚰"""
|
||||
|
||||
|
||||
_ros_node: BaseROS2DeviceNode
|
||||
|
||||
|
||||
def __init__(self, device_id: str = None, config: dict = None, **kwargs):
|
||||
"""
|
||||
初始化虚拟转移泵
|
||||
|
||||
|
||||
Args:
|
||||
device_id: 设备ID
|
||||
config: 配置字典,包含max_volume, port等参数
|
||||
**kwargs: 其他参数,确保兼容性
|
||||
"""
|
||||
self.device_id = device_id or "virtual_transfer_pump"
|
||||
|
||||
|
||||
# 从config或kwargs中获取参数,确保类型正确
|
||||
if config:
|
||||
self.max_volume = float(config.get('max_volume', 25.0))
|
||||
self.port = config.get('port', 'VIRTUAL')
|
||||
self.max_volume = float(config.get("max_volume", 25.0))
|
||||
self.port = config.get("port", "VIRTUAL")
|
||||
else:
|
||||
self.max_volume = float(kwargs.get('max_volume', 25.0))
|
||||
self.port = kwargs.get('port', 'VIRTUAL')
|
||||
|
||||
self._transfer_rate = float(kwargs.get('transfer_rate', 0))
|
||||
self.mode = kwargs.get('mode', VirtualPumpMode.Normal)
|
||||
|
||||
self.max_volume = float(kwargs.get("max_volume", 25.0))
|
||||
self.port = kwargs.get("port", "VIRTUAL")
|
||||
|
||||
self._transfer_rate = float(kwargs.get("transfer_rate", 0))
|
||||
self.mode = kwargs.get("mode", VirtualPumpMode.Normal)
|
||||
|
||||
# 状态变量 - 确保都是正确类型
|
||||
self._status = "Idle"
|
||||
self._position = 0.0 # float
|
||||
self._max_velocity = 5.0 # float
|
||||
self._max_velocity = 5.0 # float
|
||||
self._current_volume = 0.0 # float
|
||||
|
||||
# 🚀 新增:快速模式设置 - 大幅缩短执行时间
|
||||
@@ -52,14 +52,16 @@ class VirtualTransferPump:
|
||||
self._fast_dispense_time = 1.0 # 快速喷射时间(秒)
|
||||
|
||||
self.logger = logging.getLogger(f"VirtualTransferPump.{self.device_id}")
|
||||
|
||||
|
||||
print(f"🚰 === 虚拟转移泵 {self.device_id} 已创建 === ✨")
|
||||
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}")
|
||||
|
||||
|
||||
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||
self._ros_node = ros_node
|
||||
|
||||
|
||||
async def initialize(self) -> bool:
|
||||
"""初始化虚拟泵 🚀"""
|
||||
self.logger.info(f"🔧 初始化虚拟转移泵 {self.device_id} ✨")
|
||||
@@ -68,33 +70,33 @@ class VirtualTransferPump:
|
||||
self._current_volume = 0.0
|
||||
self.logger.info(f"✅ 转移泵 {self.device_id} 初始化完成 🚰")
|
||||
return True
|
||||
|
||||
|
||||
async def cleanup(self) -> bool:
|
||||
"""清理虚拟泵 🧹"""
|
||||
self.logger.info(f"🧹 清理虚拟转移泵 {self.device_id} 🔚")
|
||||
self._status = "Idle"
|
||||
self.logger.info(f"✅ 转移泵 {self.device_id} 清理完成 💤")
|
||||
return True
|
||||
|
||||
|
||||
# 基本属性
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self._status
|
||||
|
||||
|
||||
@property
|
||||
def position(self) -> float:
|
||||
"""当前柱塞位置 (ml) 📍"""
|
||||
return self._position
|
||||
|
||||
|
||||
@property
|
||||
def current_volume(self) -> float:
|
||||
"""当前注射器中的体积 (ml) 💧"""
|
||||
return self._current_volume
|
||||
|
||||
|
||||
@property
|
||||
def max_velocity(self) -> float:
|
||||
return self._max_velocity
|
||||
|
||||
|
||||
@property
|
||||
def transfer_rate(self) -> float:
|
||||
return self._transfer_rate
|
||||
@@ -103,17 +105,17 @@ class VirtualTransferPump:
|
||||
"""设置最大速度 (ml/s) 🌊"""
|
||||
self._max_velocity = max(0.1, min(50.0, velocity)) # 限制在合理范围内
|
||||
self.logger.info(f"🌊 设置最大速度为 {self._max_velocity} mL/s")
|
||||
|
||||
|
||||
def get_status(self) -> str:
|
||||
"""获取泵状态 📋"""
|
||||
return self._status
|
||||
|
||||
|
||||
async def _simulate_operation(self, duration: float):
|
||||
"""模拟操作延时 ⏱️"""
|
||||
self._status = "Busy"
|
||||
await self._ros_node.sleep(duration)
|
||||
self._status = "Idle"
|
||||
|
||||
|
||||
def _calculate_duration(self, volume: float, velocity: float = None) -> float:
|
||||
"""
|
||||
计算操作持续时间 ⏰
|
||||
@@ -121,10 +123,10 @@ class VirtualTransferPump:
|
||||
"""
|
||||
if velocity is None:
|
||||
velocity = self._max_velocity
|
||||
|
||||
|
||||
# 📊 计算理论时间(用于日志显示)
|
||||
theoretical_duration = abs(volume) / velocity
|
||||
|
||||
|
||||
# 🚀 如果启用快速模式,使用固定的快速时间
|
||||
if self._fast_mode:
|
||||
# 根据操作类型选择快速时间
|
||||
@@ -132,13 +134,13 @@ class VirtualTransferPump:
|
||||
actual_duration = self._fast_move_time
|
||||
else: # 很小的操作
|
||||
actual_duration = 0.5
|
||||
|
||||
|
||||
self.logger.debug(f"⚡ 快速模式: 理论时间 {theoretical_duration:.2f}s → 实际时间 {actual_duration:.2f}s")
|
||||
return actual_duration
|
||||
else:
|
||||
# 正常模式使用理论时间
|
||||
return theoretical_duration
|
||||
|
||||
|
||||
def _calculate_display_duration(self, volume: float, velocity: float = None) -> float:
|
||||
"""
|
||||
计算显示用的持续时间(用于日志) 📊
|
||||
@@ -147,16 +149,16 @@ class VirtualTransferPump:
|
||||
if velocity is None:
|
||||
velocity = self._max_velocity
|
||||
return abs(volume) / velocity
|
||||
|
||||
|
||||
# 新的set_position方法 - 专门用于SetPumpPosition动作
|
||||
async def set_position(self, position: float, max_velocity: float = None):
|
||||
"""
|
||||
移动到绝对位置 - 专门用于SetPumpPosition动作 🎯
|
||||
|
||||
|
||||
Args:
|
||||
position (float): 目标位置 (ml)
|
||||
max_velocity (float): 移动速度 (ml/s)
|
||||
|
||||
|
||||
Returns:
|
||||
dict: 符合SetPumpPosition.action定义的结果
|
||||
"""
|
||||
@@ -164,19 +166,19 @@ class VirtualTransferPump:
|
||||
# 验证并转换参数
|
||||
target_position = float(position)
|
||||
velocity = float(max_velocity) if max_velocity is not None else self._max_velocity
|
||||
|
||||
|
||||
# 限制位置在有效范围内
|
||||
target_position = max(0.0, min(float(self.max_volume), target_position))
|
||||
|
||||
|
||||
# 计算移动距离
|
||||
volume_to_move = abs(target_position - self._position)
|
||||
|
||||
|
||||
# 📊 计算显示用的时间(用于日志)
|
||||
display_duration = self._calculate_display_duration(volume_to_move, velocity)
|
||||
|
||||
|
||||
# ⚡ 计算实际执行时间(快速模式)
|
||||
actual_duration = self._calculate_duration(volume_to_move, velocity)
|
||||
|
||||
|
||||
# 🎯 确定操作类型和emoji
|
||||
if target_position > self._position:
|
||||
operation_type = "吸液"
|
||||
@@ -187,28 +189,34 @@ class VirtualTransferPump:
|
||||
else:
|
||||
operation_type = "保持"
|
||||
operation_emoji = "📍"
|
||||
|
||||
|
||||
self.logger.info(f"🎯 SET_POSITION: {operation_type} {operation_emoji}")
|
||||
self.logger.info(f" 📍 位置: {self._position:.2f}mL → {target_position:.2f}mL (移动 {volume_to_move:.2f}mL)")
|
||||
self.logger.info(
|
||||
f" 📍 位置: {self._position:.2f}mL → {target_position:.2f}mL (移动 {volume_to_move:.2f}mL)"
|
||||
)
|
||||
self.logger.info(f" 🌊 速度: {velocity:.2f} mL/s")
|
||||
self.logger.info(f" ⏰ 预计时间: {display_duration:.2f}s")
|
||||
|
||||
|
||||
if self._fast_mode:
|
||||
self.logger.info(f" ⚡ 快速模式: 实际用时 {actual_duration:.2f}s")
|
||||
|
||||
|
||||
# 🚀 模拟移动过程
|
||||
if volume_to_move > 0.01: # 只有当移动距离足够大时才显示进度
|
||||
start_position = self._position
|
||||
steps = 5 if actual_duration > 0.5 else 2 # 根据实际时间调整步数
|
||||
step_duration = actual_duration / steps
|
||||
|
||||
|
||||
self.logger.info(f"🚀 开始{operation_type}... {operation_emoji}")
|
||||
|
||||
|
||||
for i in range(steps + 1):
|
||||
# 计算当前位置和进度
|
||||
progress = (i / steps) * 100 if steps > 0 else 100
|
||||
current_pos = start_position + (target_position - start_position) * (i / steps) if steps > 0 else target_position
|
||||
|
||||
current_pos = (
|
||||
start_position + (target_position - start_position) * (i / steps)
|
||||
if steps > 0
|
||||
else target_position
|
||||
)
|
||||
|
||||
# 更新状态
|
||||
if i < steps:
|
||||
self._status = f"{operation_type}中"
|
||||
@@ -216,10 +224,10 @@ class VirtualTransferPump:
|
||||
else:
|
||||
self._status = "Idle"
|
||||
status_emoji = "✅"
|
||||
|
||||
|
||||
self._position = current_pos
|
||||
self._current_volume = current_pos
|
||||
|
||||
|
||||
# 显示进度(每25%或最后一步)
|
||||
if i == 0:
|
||||
self.logger.debug(f" 🔄 {operation_type}开始: {progress:.0f}%")
|
||||
@@ -227,7 +235,7 @@ class VirtualTransferPump:
|
||||
self.logger.debug(f" 🔄 {operation_type}进度: {progress:.0f}%")
|
||||
elif i == steps:
|
||||
self.logger.info(f" ✅ {operation_type}完成: {progress:.0f}% | 当前位置: {current_pos:.2f}mL")
|
||||
|
||||
|
||||
# 等待一小步时间
|
||||
if i < steps and step_duration > 0:
|
||||
await self._ros_node.sleep(step_duration)
|
||||
@@ -236,25 +244,27 @@ class VirtualTransferPump:
|
||||
self._position = target_position
|
||||
self._current_volume = target_position
|
||||
self.logger.info(f" 📍 微调完成: {target_position:.2f}mL")
|
||||
|
||||
|
||||
# 确保最终位置准确
|
||||
self._position = target_position
|
||||
self._current_volume = target_position
|
||||
self._status = "Idle"
|
||||
|
||||
|
||||
# 📊 最终状态日志
|
||||
if volume_to_move > 0.01:
|
||||
self.logger.info(f"🎉 SET_POSITION 完成! 📍 最终位置: {self._position:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL")
|
||||
|
||||
self.logger.info(
|
||||
f"🎉 SET_POSITION 完成! 📍 最终位置: {self._position:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL"
|
||||
)
|
||||
|
||||
# 返回符合action定义的结果
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"✅ 成功移动到位置 {self._position:.2f}mL ({operation_type})",
|
||||
"final_position": self._position,
|
||||
"final_volume": self._current_volume,
|
||||
"operation_type": operation_type
|
||||
"operation_type": operation_type,
|
||||
}
|
||||
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"❌ 设置位置失败: {str(e)}"
|
||||
self.logger.error(error_msg)
|
||||
@@ -262,134 +272,136 @@ class VirtualTransferPump:
|
||||
"success": False,
|
||||
"message": error_msg,
|
||||
"final_position": self._position,
|
||||
"final_volume": self._current_volume
|
||||
"final_volume": self._current_volume,
|
||||
}
|
||||
|
||||
|
||||
# 其他泵操作方法
|
||||
async def pull_plunger(self, volume: float, velocity: float = None):
|
||||
"""
|
||||
拉取柱塞(吸液) 📥
|
||||
|
||||
|
||||
Args:
|
||||
volume (float): 要拉取的体积 (ml)
|
||||
velocity (float): 拉取速度 (ml/s)
|
||||
"""
|
||||
new_position = min(self.max_volume, self._position + volume)
|
||||
actual_volume = new_position - self._position
|
||||
|
||||
|
||||
if actual_volume <= 0:
|
||||
self.logger.warning("⚠️ 无法吸液 - 已达到最大容量")
|
||||
return
|
||||
|
||||
|
||||
display_duration = self._calculate_display_duration(actual_volume, velocity)
|
||||
actual_duration = self._calculate_duration(actual_volume, velocity)
|
||||
|
||||
|
||||
self.logger.info(f"📥 开始吸液: {actual_volume:.2f}mL")
|
||||
self.logger.info(f" 📍 位置: {self._position:.2f}mL → {new_position:.2f}mL")
|
||||
self.logger.info(f" ⏰ 预计时间: {display_duration:.2f}s")
|
||||
|
||||
|
||||
if self._fast_mode:
|
||||
self.logger.info(f" ⚡ 快速模式: 实际用时 {actual_duration:.2f}s")
|
||||
|
||||
|
||||
await self._simulate_operation(actual_duration)
|
||||
|
||||
|
||||
self._position = new_position
|
||||
self._current_volume = new_position
|
||||
|
||||
|
||||
self.logger.info(f"✅ 吸液完成: {actual_volume:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL")
|
||||
|
||||
async def push_plunger(self, volume: float, velocity: float = None):
|
||||
"""
|
||||
推出柱塞(排液) 📤
|
||||
|
||||
|
||||
Args:
|
||||
volume (float): 要推出的体积 (ml)
|
||||
velocity (float): 推出速度 (ml/s)
|
||||
"""
|
||||
new_position = max(0, self._position - volume)
|
||||
actual_volume = self._position - new_position
|
||||
|
||||
|
||||
if actual_volume <= 0:
|
||||
self.logger.warning("⚠️ 无法排液 - 已达到最小容量")
|
||||
return
|
||||
|
||||
|
||||
display_duration = self._calculate_display_duration(actual_volume, velocity)
|
||||
actual_duration = self._calculate_duration(actual_volume, velocity)
|
||||
|
||||
|
||||
self.logger.info(f"📤 开始排液: {actual_volume:.2f}mL")
|
||||
self.logger.info(f" 📍 位置: {self._position:.2f}mL → {new_position:.2f}mL")
|
||||
self.logger.info(f" ⏰ 预计时间: {display_duration:.2f}s")
|
||||
|
||||
|
||||
if self._fast_mode:
|
||||
self.logger.info(f" ⚡ 快速模式: 实际用时 {actual_duration:.2f}s")
|
||||
|
||||
|
||||
await self._simulate_operation(actual_duration)
|
||||
|
||||
|
||||
self._position = new_position
|
||||
self._current_volume = new_position
|
||||
|
||||
|
||||
self.logger.info(f"✅ 排液完成: {actual_volume:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL")
|
||||
|
||||
# 便捷操作方法
|
||||
async def aspirate(self, volume: float, velocity: float = None):
|
||||
"""吸液操作 📥"""
|
||||
await self.pull_plunger(volume, velocity)
|
||||
|
||||
|
||||
async def dispense(self, volume: float, velocity: float = None):
|
||||
"""排液操作 📤"""
|
||||
await self.push_plunger(volume, velocity)
|
||||
|
||||
|
||||
async def transfer(self, volume: float, aspirate_velocity: float = None, dispense_velocity: float = None):
|
||||
"""转移操作(先吸后排) 🔄"""
|
||||
self.logger.info(f"🔄 开始转移操作: {volume:.2f}mL")
|
||||
|
||||
|
||||
# 吸液
|
||||
await self.aspirate(volume, aspirate_velocity)
|
||||
|
||||
|
||||
# 短暂停顿
|
||||
self.logger.debug("⏸️ 短暂停顿...")
|
||||
await self._ros_node.sleep(0.1)
|
||||
|
||||
|
||||
# 排液
|
||||
await self.dispense(volume, dispense_velocity)
|
||||
|
||||
|
||||
async def empty_syringe(self, velocity: float = None):
|
||||
"""清空注射器"""
|
||||
await self.set_position(0, velocity)
|
||||
|
||||
|
||||
async def fill_syringe(self, velocity: float = None):
|
||||
"""充满注射器"""
|
||||
await self.set_position(self.max_volume, velocity)
|
||||
|
||||
|
||||
async def stop_operation(self):
|
||||
"""停止当前操作"""
|
||||
self._status = "Idle"
|
||||
self.logger.info("Operation stopped")
|
||||
|
||||
|
||||
# 状态查询方法
|
||||
def get_position(self) -> float:
|
||||
"""获取当前位置"""
|
||||
return self._position
|
||||
|
||||
|
||||
def get_current_volume(self) -> float:
|
||||
"""获取当前体积"""
|
||||
return self._current_volume
|
||||
|
||||
|
||||
def get_remaining_capacity(self) -> float:
|
||||
"""获取剩余容量"""
|
||||
return self.max_volume - self._current_volume
|
||||
|
||||
|
||||
def is_empty(self) -> bool:
|
||||
"""检查是否为空"""
|
||||
return self._current_volume <= 0.01 # 允许小量误差
|
||||
|
||||
|
||||
def is_full(self) -> bool:
|
||||
"""检查是否已满"""
|
||||
return self._current_volume >= (self.max_volume - 0.01) # 允许小量误差
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"VirtualTransferPump({self.device_id}: {self._current_volume:.2f}/{self.max_volume} ml, {self._status})"
|
||||
|
||||
return (
|
||||
f"VirtualTransferPump({self.device_id}: {self._current_volume:.2f}/{self.max_volume} ml, {self._status})"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
@@ -398,20 +410,20 @@ class VirtualTransferPump:
|
||||
async def demo():
|
||||
"""虚拟泵使用示例"""
|
||||
pump = VirtualTransferPump("demo_pump", {"max_volume": 50.0})
|
||||
|
||||
|
||||
await pump.initialize()
|
||||
|
||||
|
||||
print(f"Initial state: {pump}")
|
||||
|
||||
|
||||
# 测试set_position方法
|
||||
result = await pump.set_position(10.0, max_velocity=2.0)
|
||||
print(f"Set position result: {result}")
|
||||
print(f"After setting position to 10ml: {pump}")
|
||||
|
||||
|
||||
# 吸液测试
|
||||
await pump.aspirate(5.0, velocity=2.0)
|
||||
print(f"After aspirating 5ml: {pump}")
|
||||
|
||||
|
||||
# 清空测试
|
||||
result = await pump.set_position(0.0)
|
||||
print(f"Empty result: {result}")
|
||||
|
||||
@@ -11,9 +11,10 @@ Virtual Workbench Device - 模拟工作台设备
|
||||
|
||||
注意:调用来自线程池,使用 threading.Lock 进行同步
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from typing import Dict, Any, Optional
|
||||
from typing import Dict, Any, Optional, List
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from threading import Lock, RLock
|
||||
@@ -22,37 +23,46 @@ from typing_extensions import TypedDict
|
||||
|
||||
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||
from unilabos.utils.decorator import not_action
|
||||
from unilabos.resources.resource_tracker import SampleUUIDsType, LabSample, RETURN_UNILABOS_SAMPLES
|
||||
|
||||
|
||||
# ============ TypedDict 返回类型定义 ============
|
||||
|
||||
|
||||
class MoveToHeatingStationResult(TypedDict):
|
||||
"""move_to_heating_station 返回类型"""
|
||||
|
||||
success: bool
|
||||
station_id: int
|
||||
material_id: str
|
||||
material_number: int
|
||||
message: str
|
||||
unilabos_samples: List[LabSample]
|
||||
|
||||
|
||||
class StartHeatingResult(TypedDict):
|
||||
"""start_heating 返回类型"""
|
||||
|
||||
success: bool
|
||||
station_id: int
|
||||
material_id: str
|
||||
material_number: int
|
||||
message: str
|
||||
unilabos_samples: List[LabSample]
|
||||
|
||||
|
||||
class MoveToOutputResult(TypedDict):
|
||||
"""move_to_output 返回类型"""
|
||||
|
||||
success: bool
|
||||
station_id: int
|
||||
material_id: str
|
||||
unilabos_samples: List[LabSample]
|
||||
|
||||
|
||||
class PrepareMaterialsResult(TypedDict):
|
||||
"""prepare_materials 返回类型 - 批量准备物料"""
|
||||
|
||||
success: bool
|
||||
count: int
|
||||
material_1: int # 物料编号1
|
||||
@@ -61,12 +71,15 @@ class PrepareMaterialsResult(TypedDict):
|
||||
material_4: int # 物料编号4
|
||||
material_5: int # 物料编号5
|
||||
message: str
|
||||
unilabos_samples: List[LabSample]
|
||||
|
||||
|
||||
# ============ 状态枚举 ============
|
||||
|
||||
|
||||
class HeatingStationState(Enum):
|
||||
"""加热台状态枚举"""
|
||||
|
||||
IDLE = "idle" # 空闲
|
||||
OCCUPIED = "occupied" # 已放置物料,等待加热
|
||||
HEATING = "heating" # 加热中
|
||||
@@ -75,6 +88,7 @@ class HeatingStationState(Enum):
|
||||
|
||||
class ArmState(Enum):
|
||||
"""机械臂状态枚举"""
|
||||
|
||||
IDLE = "idle" # 空闲
|
||||
BUSY = "busy" # 工作中
|
||||
|
||||
@@ -82,6 +96,7 @@ class ArmState(Enum):
|
||||
@dataclass
|
||||
class HeatingStation:
|
||||
"""加热台数据结构"""
|
||||
|
||||
station_id: int
|
||||
state: HeatingStationState = HeatingStationState.IDLE
|
||||
current_material: Optional[str] = None # 当前物料 (如 "A1", "A2")
|
||||
@@ -137,8 +152,7 @@ class VirtualWorkbench:
|
||||
|
||||
# 加热台状态 (station_id -> HeatingStation) - 立即初始化,不依赖initialize()
|
||||
self._heating_stations: Dict[int, HeatingStation] = {
|
||||
i: HeatingStation(station_id=i)
|
||||
for i in range(1, self.NUM_HEATING_STATIONS + 1)
|
||||
i: HeatingStation(station_id=i) for i in range(1, self.NUM_HEATING_STATIONS + 1)
|
||||
}
|
||||
self._stations_lock = RLock() # 可重入锁,保护加热台状态
|
||||
|
||||
@@ -178,14 +192,16 @@ class VirtualWorkbench:
|
||||
station.heating_progress = 0.0
|
||||
|
||||
# 初始化状态
|
||||
self.data.update({
|
||||
"status": "Ready",
|
||||
"arm_state": ArmState.IDLE.value,
|
||||
"arm_current_task": None,
|
||||
"heating_stations": self._get_stations_status(),
|
||||
"active_tasks_count": 0,
|
||||
"message": "工作台就绪",
|
||||
})
|
||||
self.data.update(
|
||||
{
|
||||
"status": "Ready",
|
||||
"arm_state": ArmState.IDLE.value,
|
||||
"arm_current_task": None,
|
||||
"heating_stations": self._get_stations_status(),
|
||||
"active_tasks_count": 0,
|
||||
"message": "工作台就绪",
|
||||
}
|
||||
)
|
||||
|
||||
self.logger.info(f"工作台初始化完成: {self.NUM_HEATING_STATIONS}个加热台就绪")
|
||||
return True
|
||||
@@ -204,12 +220,14 @@ class VirtualWorkbench:
|
||||
with self._tasks_lock:
|
||||
self._active_tasks.clear()
|
||||
|
||||
self.data.update({
|
||||
"status": "Offline",
|
||||
"arm_state": ArmState.IDLE.value,
|
||||
"heating_stations": {},
|
||||
"message": "工作台已关闭",
|
||||
})
|
||||
self.data.update(
|
||||
{
|
||||
"status": "Offline",
|
||||
"arm_state": ArmState.IDLE.value,
|
||||
"heating_stations": {},
|
||||
"message": "工作台已关闭",
|
||||
}
|
||||
)
|
||||
return True
|
||||
|
||||
def _get_stations_status(self) -> Dict[int, Dict[str, Any]]:
|
||||
@@ -227,12 +245,14 @@ class VirtualWorkbench:
|
||||
|
||||
def _update_data_status(self, message: Optional[str] = None):
|
||||
"""更新状态数据"""
|
||||
self.data.update({
|
||||
"arm_state": self._arm_state.value,
|
||||
"arm_current_task": self._arm_current_task,
|
||||
"heating_stations": self._get_stations_status(),
|
||||
"active_tasks_count": len(self._active_tasks),
|
||||
})
|
||||
self.data.update(
|
||||
{
|
||||
"arm_state": self._arm_state.value,
|
||||
"arm_current_task": self._arm_current_task,
|
||||
"heating_stations": self._get_stations_status(),
|
||||
"active_tasks_count": len(self._active_tasks),
|
||||
}
|
||||
)
|
||||
if message:
|
||||
self.data["message"] = message
|
||||
|
||||
@@ -280,6 +300,7 @@ class VirtualWorkbench:
|
||||
|
||||
def prepare_materials(
|
||||
self,
|
||||
sample_uuids: SampleUUIDsType,
|
||||
count: int = 5,
|
||||
) -> PrepareMaterialsResult:
|
||||
"""
|
||||
@@ -297,10 +318,7 @@ class VirtualWorkbench:
|
||||
# 生成物料列表 A1 - A{count}
|
||||
materials = [i for i in range(1, count + 1)]
|
||||
|
||||
self.logger.info(
|
||||
f"[准备物料] 生成 {count} 个物料: "
|
||||
f"A1-A{count} -> material_1~material_{count}"
|
||||
)
|
||||
self.logger.info(f"[准备物料] 生成 {count} 个物料: " f"A1-A{count} -> material_1~material_{count}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
@@ -311,10 +329,12 @@ class VirtualWorkbench:
|
||||
"material_4": materials[3] if len(materials) > 3 else 0,
|
||||
"material_5": materials[4] if len(materials) > 4 else 0,
|
||||
"message": f"已准备 {count} 个物料: A1-A{count}",
|
||||
"unilabos_samples": [LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for sample_uuid, content in sample_uuids.items()]
|
||||
}
|
||||
|
||||
def move_to_heating_station(
|
||||
self,
|
||||
sample_uuids: SampleUUIDsType,
|
||||
material_number: int,
|
||||
) -> MoveToHeatingStationResult:
|
||||
"""
|
||||
@@ -391,6 +411,9 @@ class VirtualWorkbench:
|
||||
"material_id": material_id,
|
||||
"material_number": material_number,
|
||||
"message": f"{material_id}已成功移动到加热台{station_id}",
|
||||
"unilabos_samples": [
|
||||
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for
|
||||
sample_uuid, content in sample_uuids.items()]
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
@@ -403,10 +426,14 @@ class VirtualWorkbench:
|
||||
"material_id": material_id,
|
||||
"material_number": material_number,
|
||||
"message": f"移动失败: {str(e)}",
|
||||
"unilabos_samples": [
|
||||
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for
|
||||
sample_uuid, content in sample_uuids.items()]
|
||||
}
|
||||
|
||||
def start_heating(
|
||||
self,
|
||||
sample_uuids: SampleUUIDsType,
|
||||
station_id: int,
|
||||
material_number: int,
|
||||
) -> StartHeatingResult:
|
||||
@@ -429,6 +456,9 @@ class VirtualWorkbench:
|
||||
"material_id": "",
|
||||
"material_number": material_number,
|
||||
"message": f"无效的加热台ID: {station_id}",
|
||||
"unilabos_samples": [
|
||||
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for
|
||||
sample_uuid, content in sample_uuids.items()]
|
||||
}
|
||||
|
||||
with self._stations_lock:
|
||||
@@ -441,6 +471,9 @@ class VirtualWorkbench:
|
||||
"material_id": "",
|
||||
"material_number": material_number,
|
||||
"message": f"加热台{station_id}上没有物料",
|
||||
"unilabos_samples": [
|
||||
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for
|
||||
sample_uuid, content in sample_uuids.items()]
|
||||
}
|
||||
|
||||
if station.state == HeatingStationState.HEATING:
|
||||
@@ -450,6 +483,9 @@ class VirtualWorkbench:
|
||||
"material_id": station.current_material,
|
||||
"material_number": material_number,
|
||||
"message": f"加热台{station_id}已经在加热中",
|
||||
"unilabos_samples": [
|
||||
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for
|
||||
sample_uuid, content in sample_uuids.items()]
|
||||
}
|
||||
|
||||
material_id = station.current_material
|
||||
@@ -499,10 +535,14 @@ class VirtualWorkbench:
|
||||
"material_id": material_id,
|
||||
"material_number": material_number,
|
||||
"message": f"加热台{station_id}加热完成",
|
||||
"unilabos_samples": [
|
||||
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for
|
||||
sample_uuid, content in sample_uuids.items()]
|
||||
}
|
||||
|
||||
def move_to_output(
|
||||
self,
|
||||
sample_uuids: SampleUUIDsType,
|
||||
station_id: int,
|
||||
material_number: int,
|
||||
) -> MoveToOutputResult:
|
||||
@@ -525,6 +565,9 @@ class VirtualWorkbench:
|
||||
"material_id": "",
|
||||
"output_position": f"C{output_number}",
|
||||
"message": f"无效的加热台ID: {station_id}",
|
||||
"unilabos_samples": [
|
||||
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for
|
||||
sample_uuid, content in sample_uuids.items()]
|
||||
}
|
||||
|
||||
with self._stations_lock:
|
||||
@@ -538,6 +581,9 @@ class VirtualWorkbench:
|
||||
"material_id": "",
|
||||
"output_position": f"C{output_number}",
|
||||
"message": f"加热台{station_id}上没有物料",
|
||||
"unilabos_samples": [
|
||||
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for
|
||||
sample_uuid, content in sample_uuids.items()]
|
||||
}
|
||||
|
||||
if station.state != HeatingStationState.COMPLETED:
|
||||
@@ -547,6 +593,9 @@ class VirtualWorkbench:
|
||||
"material_id": material_id,
|
||||
"output_position": f"C{output_number}",
|
||||
"message": f"加热台{station_id}尚未完成加热 (当前状态: {station.state.value})",
|
||||
"unilabos_samples": [
|
||||
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for
|
||||
sample_uuid, content in sample_uuids.items()]
|
||||
}
|
||||
|
||||
output_position = f"C{output_number}"
|
||||
@@ -595,6 +644,9 @@ class VirtualWorkbench:
|
||||
"material_id": material_id,
|
||||
"output_position": output_position,
|
||||
"message": f"{material_id}已成功移动到{output_position}",
|
||||
"unilabos_samples": [
|
||||
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for
|
||||
sample_uuid, content in sample_uuids.items()]
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
@@ -607,6 +659,9 @@ class VirtualWorkbench:
|
||||
"material_id": "",
|
||||
"output_position": output_position,
|
||||
"message": f"移动失败: {str(e)}",
|
||||
"unilabos_samples": [
|
||||
LabSample(sample_uuid=sample_uuid, oss_path="", extra={"material_uuid": content} if isinstance(content, str) else content.serialize()) for
|
||||
sample_uuid, content in sample_uuids.items()]
|
||||
}
|
||||
|
||||
# ============ 状态属性 ============
|
||||
|
||||
Reference in New Issue
Block a user