mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-03-24 04:10:33 +00:00
fast registry load minor fix on skill & registry stripe ros2 schema desc add create-device-skill new registry system backwards to yaml remove not exist resource new registry sys exp. support with add device add ai conventions correct raise create resource error ret info fix revert ret info fix fix prcxi check add create_resource schema re signal host ready event add websocket connection timeout and improve reconnection logic add open_timeout parameter to websocket connection add TimeoutError and InvalidStatus exception handling implement exponential backoff for reconnection attempts simplify reconnection logic flow add gzip change pose extra to any add isFlapY
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", "")
|