mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-03-24 18:59:17 +00:00
875 lines
33 KiB
Python
875 lines
33 KiB
Python
"""
|
|
Virtual Workbench Device - 模拟工作台设备
|
|
包含:
|
|
- 1个机械臂 (每次操作3s, 独占锁)
|
|
- 3个加热台 (每次加热10s, 可并行)
|
|
|
|
工作流程:
|
|
1. A1-A5 物料同时启动, 竞争机械臂
|
|
2. 机械臂将物料移动到空闲加热台
|
|
3. 加热完成后, 机械臂将物料移动到C1-C5
|
|
|
|
注意: 调用来自线程池, 使用 threading.Lock 进行同步
|
|
"""
|
|
|
|
import logging
|
|
import time
|
|
from typing import Dict, Any, Optional, List
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
from threading import Lock, RLock
|
|
|
|
from typing_extensions import TypedDict
|
|
|
|
from unilabos.registry.decorators import (
|
|
device, action, ActionInputHandle, ActionOutputHandle, DataSource, topic_config, not_action
|
|
)
|
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
|
from unilabos.resources.resource_tracker import SampleUUIDsType, LabSample
|
|
|
|
|
|
# ============ 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
|
|
output_position: str
|
|
message: str
|
|
unilabos_samples: List[LabSample]
|
|
|
|
|
|
class PrepareMaterialsResult(TypedDict):
|
|
"""prepare_materials 返回类型 - 批量准备物料"""
|
|
|
|
success: bool
|
|
count: int
|
|
material_1: int # 物料编号1
|
|
material_2: int # 物料编号2
|
|
material_3: int # 物料编号3
|
|
material_4: int # 物料编号4
|
|
material_5: int # 物料编号5
|
|
message: str
|
|
unilabos_samples: List[LabSample]
|
|
|
|
|
|
# ============ 状态枚举 ============
|
|
|
|
|
|
class HeatingStationState(Enum):
|
|
"""加热台状态枚举"""
|
|
|
|
IDLE = "idle" # 空闲
|
|
OCCUPIED = "occupied" # 已放置物料, 等待加热
|
|
HEATING = "heating" # 加热中
|
|
COMPLETED = "completed" # 加热完成, 等待取走
|
|
|
|
|
|
class ArmState(Enum):
|
|
"""机械臂状态枚举"""
|
|
|
|
IDLE = "idle" # 空闲
|
|
BUSY = "busy" # 工作中
|
|
|
|
|
|
@dataclass
|
|
class HeatingStation:
|
|
"""加热台数据结构"""
|
|
|
|
station_id: int
|
|
state: HeatingStationState = HeatingStationState.IDLE
|
|
current_material: Optional[str] = None # 当前物料 (如 "A1", "A2")
|
|
material_number: Optional[int] = None # 物料编号 (1-5)
|
|
heating_start_time: Optional[float] = None
|
|
heating_progress: float = 0.0
|
|
|
|
|
|
@device(
|
|
id="virtual_workbench",
|
|
category=["virtual_device"],
|
|
description="Virtual Workbench with 1 robotic arm and 3 heating stations for concurrent material processing",
|
|
)
|
|
class VirtualWorkbench:
|
|
"""
|
|
Virtual Workbench Device - 虚拟工作台设备
|
|
|
|
模拟一个包含1个机械臂和3个加热台的工作站
|
|
- 机械臂操作耗时3秒, 同一时间只能执行一个操作
|
|
- 加热台加热耗时10秒, 3个加热台可并行工作
|
|
|
|
工作流:
|
|
1. 物料A1-A5并发启动(线程池), 竞争机械臂使用权
|
|
2. 获取机械臂后, 查找空闲加热台
|
|
3. 机械臂将物料放入加热台, 开始加热
|
|
4. 加热完成后, 机械臂将物料移动到目标位置Cn
|
|
"""
|
|
|
|
_ros_node: BaseROS2DeviceNode
|
|
|
|
# 配置常量
|
|
ARM_OPERATION_TIME: float = 2 # 机械臂操作时间(秒)
|
|
HEATING_TIME: float = 60.0 # 加热时间(秒)
|
|
NUM_HEATING_STATIONS: int = 3 # 加热台数量
|
|
|
|
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
|
# 处理可能的不同调用方式
|
|
if device_id is None and "id" in kwargs:
|
|
device_id = kwargs.pop("id")
|
|
if config is None and "config" in kwargs:
|
|
config = kwargs.pop("config")
|
|
|
|
self.device_id = device_id or "virtual_workbench"
|
|
self.config = config or {}
|
|
|
|
self.logger = logging.getLogger(f"VirtualWorkbench.{self.device_id}")
|
|
self.data: Dict[str, Any] = {}
|
|
|
|
# 从config中获取可配置参数
|
|
self.ARM_OPERATION_TIME = float(self.config.get("arm_operation_time", self.ARM_OPERATION_TIME))
|
|
self.HEATING_TIME = float(self.config.get("heating_time", self.HEATING_TIME))
|
|
self.NUM_HEATING_STATIONS = int(self.config.get("num_heating_stations", self.NUM_HEATING_STATIONS))
|
|
|
|
# 机械臂状态和锁
|
|
self._arm_lock = Lock()
|
|
self._arm_state = ArmState.IDLE
|
|
self._arm_current_task: Optional[str] = None
|
|
|
|
# 加热台状态
|
|
self._heating_stations: Dict[int, HeatingStation] = {
|
|
i: HeatingStation(station_id=i) for i in range(1, self.NUM_HEATING_STATIONS + 1)
|
|
}
|
|
self._stations_lock = RLock()
|
|
|
|
# 任务追踪
|
|
self._active_tasks: Dict[str, Dict[str, Any]] = {}
|
|
self._tasks_lock = Lock()
|
|
|
|
# 处理其他kwargs参数
|
|
skip_keys = {"arm_operation_time", "heating_time", "num_heating_stations"}
|
|
for key, value in kwargs.items():
|
|
if key not in skip_keys and not hasattr(self, key):
|
|
setattr(self, key, value)
|
|
|
|
self.logger.info(f"=== 虚拟工作台 {self.device_id} 已创建 ===")
|
|
self.logger.info(
|
|
f"机械臂操作时间: {self.ARM_OPERATION_TIME}s | "
|
|
f"加热时间: {self.HEATING_TIME}s | "
|
|
f"加热台数量: {self.NUM_HEATING_STATIONS}"
|
|
)
|
|
|
|
@not_action
|
|
def post_init(self, ros_node: BaseROS2DeviceNode):
|
|
"""ROS节点初始化后回调"""
|
|
self._ros_node = ros_node
|
|
|
|
@not_action
|
|
def initialize(self) -> bool:
|
|
"""初始化虚拟工作台"""
|
|
self.logger.info(f"初始化虚拟工作台 {self.device_id}")
|
|
|
|
with self._stations_lock:
|
|
for station in self._heating_stations.values():
|
|
station.state = HeatingStationState.IDLE
|
|
station.current_material = None
|
|
station.material_number = None
|
|
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.logger.info(f"工作台初始化完成: {self.NUM_HEATING_STATIONS}个加热台就绪")
|
|
return True
|
|
|
|
@not_action
|
|
def cleanup(self) -> bool:
|
|
"""清理虚拟工作台"""
|
|
self.logger.info(f"清理虚拟工作台 {self.device_id}")
|
|
|
|
self._arm_state = ArmState.IDLE
|
|
self._arm_current_task = None
|
|
|
|
with self._stations_lock:
|
|
self._heating_stations.clear()
|
|
|
|
with self._tasks_lock:
|
|
self._active_tasks.clear()
|
|
|
|
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]]:
|
|
"""获取所有加热台状态"""
|
|
with self._stations_lock:
|
|
return {
|
|
station_id: {
|
|
"state": station.state.value,
|
|
"current_material": station.current_material,
|
|
"material_number": station.material_number,
|
|
"heating_progress": station.heating_progress,
|
|
}
|
|
for station_id, station in self._heating_stations.items()
|
|
}
|
|
|
|
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),
|
|
}
|
|
)
|
|
if message:
|
|
self.data["message"] = message
|
|
|
|
def _find_available_heating_station(self) -> Optional[int]:
|
|
"""查找空闲的加热台"""
|
|
with self._stations_lock:
|
|
for station_id, station in self._heating_stations.items():
|
|
if station.state == HeatingStationState.IDLE:
|
|
return station_id
|
|
return None
|
|
|
|
def _acquire_arm(self, task_description: str) -> bool:
|
|
"""获取机械臂使用权(阻塞直到获取)"""
|
|
self.logger.info(f"[{task_description}] 等待获取机械臂...")
|
|
self._arm_lock.acquire()
|
|
self._arm_state = ArmState.BUSY
|
|
self._arm_current_task = task_description
|
|
self._update_data_status(f"机械臂执行: {task_description}")
|
|
self.logger.info(f"[{task_description}] 成功获取机械臂使用权")
|
|
return True
|
|
|
|
def _release_arm(self):
|
|
"""释放机械臂"""
|
|
task = self._arm_current_task
|
|
self._arm_state = ArmState.IDLE
|
|
self._arm_current_task = None
|
|
self._arm_lock.release()
|
|
self._update_data_status(f"机械臂已释放 (完成: {task})")
|
|
self.logger.info(f"机械臂已释放 (完成: {task})")
|
|
|
|
@action(
|
|
auto_prefix=True,
|
|
description="批量准备物料 - 虚拟起始节点, 生成A1-A5物料, 输出5个handle供后续节点使用",
|
|
handles=[
|
|
ActionOutputHandle(key="channel_1", data_type="workbench_material",
|
|
label="实验1", data_key="material_1", data_source=DataSource.EXECUTOR),
|
|
ActionOutputHandle(key="channel_2", data_type="workbench_material",
|
|
label="实验2", data_key="material_2", data_source=DataSource.EXECUTOR),
|
|
ActionOutputHandle(key="channel_3", data_type="workbench_material",
|
|
label="实验3", data_key="material_3", data_source=DataSource.EXECUTOR),
|
|
ActionOutputHandle(key="channel_4", data_type="workbench_material",
|
|
label="实验4", data_key="material_4", data_source=DataSource.EXECUTOR),
|
|
ActionOutputHandle(key="channel_5", data_type="workbench_material",
|
|
label="实验5", data_key="material_5", data_source=DataSource.EXECUTOR),
|
|
],
|
|
)
|
|
def prepare_materials(
|
|
self,
|
|
sample_uuids: SampleUUIDsType,
|
|
count: int = 5,
|
|
) -> PrepareMaterialsResult:
|
|
"""
|
|
批量准备物料 - 虚拟起始节点
|
|
|
|
作为工作流的起始节点, 生成指定数量的物料编号供后续节点使用。
|
|
输出5个handle (material_1 ~ material_5), 分别对应实验1~5。
|
|
"""
|
|
materials = [i for i in range(1, count + 1)]
|
|
|
|
self.logger.info(
|
|
f"[准备物料] 生成 {count} 个物料: A1-A{count} -> material_1~material_{count}"
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"count": count,
|
|
"material_1": materials[0] if len(materials) > 0 else 0,
|
|
"material_2": materials[1] if len(materials) > 1 else 0,
|
|
"material_3": materials[2] if len(materials) > 2 else 0,
|
|
"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() if content else {}),
|
|
)
|
|
for sample_uuid, content in sample_uuids.items()
|
|
],
|
|
}
|
|
|
|
@action(
|
|
auto_prefix=True,
|
|
description="将物料从An位置移动到空闲加热台, 返回分配的加热台ID",
|
|
handles=[
|
|
ActionInputHandle(key="material_input", data_type="workbench_material",
|
|
label="物料编号", data_key="material_number", data_source=DataSource.HANDLE),
|
|
ActionOutputHandle(key="heating_station_output", data_type="workbench_station",
|
|
label="加热台ID", data_key="station_id", data_source=DataSource.EXECUTOR),
|
|
ActionOutputHandle(key="material_number_output", data_type="workbench_material",
|
|
label="物料编号", data_key="material_number", data_source=DataSource.EXECUTOR),
|
|
],
|
|
)
|
|
def move_to_heating_station(
|
|
self,
|
|
sample_uuids: SampleUUIDsType,
|
|
material_number: int,
|
|
) -> MoveToHeatingStationResult:
|
|
"""
|
|
将物料从An位置移动到加热台
|
|
|
|
多线程并发调用时, 会竞争机械臂使用权, 并自动查找空闲加热台
|
|
"""
|
|
material_id = f"A{material_number}"
|
|
task_desc = f"移动{material_id}到加热台"
|
|
self.logger.info(f"[任务] {task_desc} - 开始执行")
|
|
|
|
with self._tasks_lock:
|
|
self._active_tasks[material_id] = {
|
|
"status": "waiting_for_arm",
|
|
"start_time": time.time(),
|
|
}
|
|
|
|
try:
|
|
with self._tasks_lock:
|
|
self._active_tasks[material_id]["status"] = "waiting_for_arm"
|
|
self._acquire_arm(task_desc)
|
|
|
|
with self._tasks_lock:
|
|
self._active_tasks[material_id]["status"] = "finding_station"
|
|
station_id = None
|
|
|
|
while station_id is None:
|
|
station_id = self._find_available_heating_station()
|
|
if station_id is None:
|
|
self.logger.info(f"[{material_id}] 没有空闲加热台, 等待中...")
|
|
self._release_arm()
|
|
time.sleep(0.5)
|
|
self._acquire_arm(task_desc)
|
|
|
|
with self._stations_lock:
|
|
self._heating_stations[station_id].state = HeatingStationState.OCCUPIED
|
|
self._heating_stations[station_id].current_material = material_id
|
|
self._heating_stations[station_id].material_number = material_number
|
|
|
|
with self._tasks_lock:
|
|
self._active_tasks[material_id]["status"] = "arm_moving"
|
|
self._active_tasks[material_id]["assigned_station"] = station_id
|
|
self.logger.info(f"[{material_id}] 机械臂正在移动到加热台{station_id}...")
|
|
|
|
time.sleep(self.ARM_OPERATION_TIME)
|
|
|
|
self._update_data_status(f"{material_id}已放入加热台{station_id}")
|
|
self.logger.info(
|
|
f"[{material_id}] 已放入加热台{station_id} (用时{self.ARM_OPERATION_TIME}s)"
|
|
)
|
|
|
|
self._release_arm()
|
|
|
|
with self._tasks_lock:
|
|
self._active_tasks[material_id]["status"] = "placed_on_station"
|
|
|
|
return {
|
|
"success": True,
|
|
"station_id": station_id,
|
|
"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() if content else {})
|
|
),
|
|
)
|
|
for sample_uuid, content in sample_uuids.items()
|
|
],
|
|
}
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"[{material_id}] 移动失败: {str(e)}")
|
|
if self._arm_lock.locked():
|
|
self._release_arm()
|
|
return {
|
|
"success": False,
|
|
"station_id": -1,
|
|
"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() if content else {})
|
|
),
|
|
)
|
|
for sample_uuid, content in sample_uuids.items()
|
|
],
|
|
}
|
|
|
|
@action(
|
|
auto_prefix=True,
|
|
always_free=True,
|
|
description="启动指定加热台的加热程序",
|
|
handles=[
|
|
ActionInputHandle(key="station_id_input", data_type="workbench_station",
|
|
label="加热台ID", data_key="station_id", data_source=DataSource.HANDLE),
|
|
ActionInputHandle(key="material_number_input", data_type="workbench_material",
|
|
label="物料编号", data_key="material_number", data_source=DataSource.HANDLE),
|
|
ActionOutputHandle(key="heating_done_station", data_type="workbench_station",
|
|
label="加热完成-加热台ID", data_key="station_id", data_source=DataSource.EXECUTOR),
|
|
ActionOutputHandle(key="heating_done_material", data_type="workbench_material",
|
|
label="加热完成-物料编号", data_key="material_number", data_source=DataSource.EXECUTOR),
|
|
],
|
|
)
|
|
def start_heating(
|
|
self,
|
|
sample_uuids: SampleUUIDsType,
|
|
station_id: int,
|
|
material_number: int,
|
|
) -> StartHeatingResult:
|
|
"""
|
|
启动指定加热台的加热程序
|
|
"""
|
|
self.logger.info(f"[加热台{station_id}] 开始加热")
|
|
|
|
if station_id not in self._heating_stations:
|
|
return {
|
|
"success": False,
|
|
"station_id": station_id,
|
|
"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() if content else {})
|
|
),
|
|
)
|
|
for sample_uuid, content in sample_uuids.items()
|
|
],
|
|
}
|
|
|
|
with self._stations_lock:
|
|
station = self._heating_stations[station_id]
|
|
|
|
if station.current_material is None:
|
|
return {
|
|
"success": False,
|
|
"station_id": station_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() if content else {})
|
|
),
|
|
)
|
|
for sample_uuid, content in sample_uuids.items()
|
|
],
|
|
}
|
|
|
|
if station.state == HeatingStationState.HEATING:
|
|
return {
|
|
"success": False,
|
|
"station_id": station_id,
|
|
"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() if content else {})
|
|
),
|
|
)
|
|
for sample_uuid, content in sample_uuids.items()
|
|
],
|
|
}
|
|
|
|
material_id = station.current_material
|
|
|
|
station.state = HeatingStationState.HEATING
|
|
station.heating_start_time = time.time()
|
|
station.heating_progress = 0.0
|
|
|
|
with self._tasks_lock:
|
|
if material_id in self._active_tasks:
|
|
self._active_tasks[material_id]["status"] = "heating"
|
|
|
|
self._update_data_status(f"加热台{station_id}开始加热{material_id}")
|
|
|
|
with self._stations_lock:
|
|
heating_list = [
|
|
f"加热台{sid}:{s.current_material}"
|
|
for sid, s in self._heating_stations.items()
|
|
if s.state == HeatingStationState.HEATING and s.current_material
|
|
]
|
|
self.logger.info(f"[并行加热] 当前同时加热中: {', '.join(heating_list)}")
|
|
|
|
start_time = time.time()
|
|
last_countdown_log = start_time
|
|
while True:
|
|
elapsed = time.time() - start_time
|
|
remaining = max(0.0, self.HEATING_TIME - elapsed)
|
|
progress = min(100.0, (elapsed / self.HEATING_TIME) * 100)
|
|
|
|
with self._stations_lock:
|
|
self._heating_stations[station_id].heating_progress = progress
|
|
|
|
self._update_data_status(f"加热台{station_id}加热中: {progress:.1f}%")
|
|
|
|
if time.time() - last_countdown_log >= 5.0:
|
|
self.logger.info(f"[加热台{station_id}] {material_id} 剩余 {remaining:.1f}s")
|
|
last_countdown_log = time.time()
|
|
|
|
if elapsed >= self.HEATING_TIME:
|
|
break
|
|
|
|
time.sleep(1.0)
|
|
|
|
with self._stations_lock:
|
|
self._heating_stations[station_id].state = HeatingStationState.COMPLETED
|
|
self._heating_stations[station_id].heating_progress = 100.0
|
|
|
|
with self._tasks_lock:
|
|
if material_id in self._active_tasks:
|
|
self._active_tasks[material_id]["status"] = "heating_completed"
|
|
|
|
self._update_data_status(f"加热台{station_id}加热完成")
|
|
self.logger.info(f"[加热台{station_id}] {material_id}加热完成 (用时{self.HEATING_TIME}s)")
|
|
|
|
return {
|
|
"success": True,
|
|
"station_id": station_id,
|
|
"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() if content else {})
|
|
),
|
|
)
|
|
for sample_uuid, content in sample_uuids.items()
|
|
],
|
|
}
|
|
|
|
@action(
|
|
auto_prefix=True,
|
|
description="将物料从加热台移动到输出位置Cn",
|
|
handles=[
|
|
ActionInputHandle(key="output_station_input", data_type="workbench_station",
|
|
label="加热台ID", data_key="station_id", data_source=DataSource.HANDLE),
|
|
ActionInputHandle(key="output_material_input", data_type="workbench_material",
|
|
label="物料编号", data_key="material_number", data_source=DataSource.HANDLE),
|
|
],
|
|
)
|
|
def move_to_output(
|
|
self,
|
|
sample_uuids: SampleUUIDsType,
|
|
station_id: int,
|
|
material_number: int,
|
|
) -> MoveToOutputResult:
|
|
"""
|
|
将物料从加热台移动到输出位置Cn
|
|
"""
|
|
output_number = material_number
|
|
|
|
if station_id not in self._heating_stations:
|
|
return {
|
|
"success": False,
|
|
"station_id": station_id,
|
|
"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() if content else {})
|
|
),
|
|
)
|
|
for sample_uuid, content in sample_uuids.items()
|
|
],
|
|
}
|
|
|
|
with self._stations_lock:
|
|
station = self._heating_stations[station_id]
|
|
material_id = station.current_material
|
|
|
|
if material_id is None:
|
|
return {
|
|
"success": False,
|
|
"station_id": station_id,
|
|
"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() if content else {})
|
|
),
|
|
)
|
|
for sample_uuid, content in sample_uuids.items()
|
|
],
|
|
}
|
|
|
|
if station.state != HeatingStationState.COMPLETED:
|
|
return {
|
|
"success": False,
|
|
"station_id": station_id,
|
|
"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() if content else {})
|
|
),
|
|
)
|
|
for sample_uuid, content in sample_uuids.items()
|
|
],
|
|
}
|
|
|
|
output_position = f"C{output_number}"
|
|
task_desc = f"从加热台{station_id}移动{material_id}到{output_position}"
|
|
self.logger.info(f"[任务] {task_desc}")
|
|
|
|
try:
|
|
with self._tasks_lock:
|
|
if material_id in self._active_tasks:
|
|
self._active_tasks[material_id]["status"] = "waiting_for_arm_output"
|
|
|
|
self._acquire_arm(task_desc)
|
|
|
|
with self._tasks_lock:
|
|
if material_id in self._active_tasks:
|
|
self._active_tasks[material_id]["status"] = "arm_moving_to_output"
|
|
|
|
self.logger.info(
|
|
f"[{material_id}] 机械臂正在从加热台{station_id}取出并移动到{output_position}..."
|
|
)
|
|
time.sleep(self.ARM_OPERATION_TIME)
|
|
|
|
with self._stations_lock:
|
|
self._heating_stations[station_id].state = HeatingStationState.IDLE
|
|
self._heating_stations[station_id].current_material = None
|
|
self._heating_stations[station_id].material_number = None
|
|
self._heating_stations[station_id].heating_progress = 0.0
|
|
self._heating_stations[station_id].heating_start_time = None
|
|
|
|
self._release_arm()
|
|
|
|
with self._tasks_lock:
|
|
if material_id in self._active_tasks:
|
|
self._active_tasks[material_id]["status"] = "completed"
|
|
self._active_tasks[material_id]["end_time"] = time.time()
|
|
|
|
self._update_data_status(f"{material_id}已移动到{output_position}")
|
|
self.logger.info(
|
|
f"[{material_id}] 已成功移动到{output_position} (用时{self.ARM_OPERATION_TIME}s)"
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"station_id": station_id,
|
|
"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() if content is not None else {})
|
|
),
|
|
)
|
|
for sample_uuid, content in sample_uuids.items()
|
|
],
|
|
}
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"移动到输出位置失败: {str(e)}")
|
|
if self._arm_lock.locked():
|
|
self._release_arm()
|
|
return {
|
|
"success": False,
|
|
"station_id": station_id,
|
|
"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() if content else {})
|
|
),
|
|
)
|
|
for sample_uuid, content in sample_uuids.items()
|
|
],
|
|
}
|
|
|
|
# ============ 状态属性 ============
|
|
|
|
@property
|
|
@topic_config()
|
|
def status(self) -> str:
|
|
return self.data.get("status", "Unknown")
|
|
|
|
@property
|
|
@topic_config()
|
|
def arm_state(self) -> str:
|
|
return self._arm_state.value
|
|
|
|
@property
|
|
@topic_config()
|
|
def arm_current_task(self) -> str:
|
|
return self._arm_current_task or ""
|
|
|
|
@property
|
|
@topic_config()
|
|
def heating_station_1_state(self) -> str:
|
|
with self._stations_lock:
|
|
station = self._heating_stations.get(1)
|
|
return station.state.value if station else "unknown"
|
|
|
|
@property
|
|
@topic_config()
|
|
def heating_station_1_material(self) -> str:
|
|
with self._stations_lock:
|
|
station = self._heating_stations.get(1)
|
|
return station.current_material or "" if station else ""
|
|
|
|
@property
|
|
@topic_config()
|
|
def heating_station_1_progress(self) -> float:
|
|
with self._stations_lock:
|
|
station = self._heating_stations.get(1)
|
|
return station.heating_progress if station else 0.0
|
|
|
|
@property
|
|
@topic_config()
|
|
def heating_station_2_state(self) -> str:
|
|
with self._stations_lock:
|
|
station = self._heating_stations.get(2)
|
|
return station.state.value if station else "unknown"
|
|
|
|
@property
|
|
@topic_config()
|
|
def heating_station_2_material(self) -> str:
|
|
with self._stations_lock:
|
|
station = self._heating_stations.get(2)
|
|
return station.current_material or "" if station else ""
|
|
|
|
@property
|
|
@topic_config()
|
|
def heating_station_2_progress(self) -> float:
|
|
with self._stations_lock:
|
|
station = self._heating_stations.get(2)
|
|
return station.heating_progress if station else 0.0
|
|
|
|
@property
|
|
@topic_config()
|
|
def heating_station_3_state(self) -> str:
|
|
with self._stations_lock:
|
|
station = self._heating_stations.get(3)
|
|
return station.state.value if station else "unknown"
|
|
|
|
@property
|
|
@topic_config()
|
|
def heating_station_3_material(self) -> str:
|
|
with self._stations_lock:
|
|
station = self._heating_stations.get(3)
|
|
return station.current_material or "" if station else ""
|
|
|
|
@property
|
|
@topic_config()
|
|
def heating_station_3_progress(self) -> float:
|
|
with self._stations_lock:
|
|
station = self._heating_stations.get(3)
|
|
return station.heating_progress if station else 0.0
|
|
|
|
@property
|
|
@topic_config()
|
|
def active_tasks_count(self) -> int:
|
|
with self._tasks_lock:
|
|
return len(self._active_tasks)
|
|
|
|
@property
|
|
@topic_config()
|
|
def message(self) -> str:
|
|
return self.data.get("message", "")
|