mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-03-24 09:49:16 +00:00
v0.10.19
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
This commit is contained in:
@@ -1,15 +1,15 @@
|
||||
"""
|
||||
Virtual Workbench Device - 模拟工作台设备
|
||||
包含:
|
||||
包含:
|
||||
- 1个机械臂 (每次操作3s, 独占锁)
|
||||
- 3个加热台 (每次加热10s, 可并行)
|
||||
|
||||
工作流程:
|
||||
1. A1-A5 物料同时启动,竞争机械臂
|
||||
工作流程:
|
||||
1. A1-A5 物料同时启动, 竞争机械臂
|
||||
2. 机械臂将物料移动到空闲加热台
|
||||
3. 加热完成后,机械臂将物料移动到C1-C5
|
||||
3. 加热完成后, 机械臂将物料移动到C1-C5
|
||||
|
||||
注意:调用来自线程池,使用 threading.Lock 进行同步
|
||||
注意: 调用来自线程池, 使用 threading.Lock 进行同步
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -21,9 +21,11 @@ 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.utils.decorator import not_action, always_free
|
||||
from unilabos.resources.resource_tracker import SampleUUIDsType, LabSample, RETURN_UNILABOS_SAMPLES
|
||||
from unilabos.resources.resource_tracker import SampleUUIDsType, LabSample
|
||||
|
||||
|
||||
# ============ TypedDict 返回类型定义 ============
|
||||
@@ -57,6 +59,8 @@ class MoveToOutputResult(TypedDict):
|
||||
success: bool
|
||||
station_id: int
|
||||
material_id: str
|
||||
output_position: str
|
||||
message: str
|
||||
unilabos_samples: List[LabSample]
|
||||
|
||||
|
||||
@@ -81,9 +85,9 @@ class HeatingStationState(Enum):
|
||||
"""加热台状态枚举"""
|
||||
|
||||
IDLE = "idle" # 空闲
|
||||
OCCUPIED = "occupied" # 已放置物料,等待加热
|
||||
OCCUPIED = "occupied" # 已放置物料, 等待加热
|
||||
HEATING = "heating" # 加热中
|
||||
COMPLETED = "completed" # 加热完成,等待取走
|
||||
COMPLETED = "completed" # 加热完成, 等待取走
|
||||
|
||||
|
||||
class ArmState(Enum):
|
||||
@@ -105,19 +109,24 @@ class HeatingStation:
|
||||
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个加热台可并行工作
|
||||
- 机械臂操作耗时3秒, 同一时间只能执行一个操作
|
||||
- 加热台加热耗时10秒, 3个加热台可并行工作
|
||||
|
||||
工作流:
|
||||
1. 物料A1-A5并发启动(线程池),竞争机械臂使用权
|
||||
2. 获取机械臂后,查找空闲加热台
|
||||
3. 机械臂将物料放入加热台,开始加热
|
||||
4. 加热完成后,机械臂将物料移动到目标位置Cn
|
||||
1. 物料A1-A5并发启动(线程池), 竞争机械臂使用权
|
||||
2. 获取机械臂后, 查找空闲加热台
|
||||
3. 机械臂将物料放入加热台, 开始加热
|
||||
4. 加热完成后, 机械臂将物料移动到目标位置Cn
|
||||
"""
|
||||
|
||||
_ros_node: BaseROS2DeviceNode
|
||||
@@ -145,19 +154,19 @@ class VirtualWorkbench:
|
||||
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))
|
||||
|
||||
# 机械臂状态和锁 (使用threading.Lock)
|
||||
# 机械臂状态和锁
|
||||
self._arm_lock = Lock()
|
||||
self._arm_state = ArmState.IDLE
|
||||
self._arm_current_task: Optional[str] = None
|
||||
|
||||
# 加热台状态 (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)
|
||||
}
|
||||
self._stations_lock = RLock() # 可重入锁,保护加热台状态
|
||||
self._stations_lock = RLock()
|
||||
|
||||
# 任务追踪
|
||||
self._active_tasks: Dict[str, Dict[str, Any]] = {} # material_id -> task_info
|
||||
self._active_tasks: Dict[str, Dict[str, Any]] = {}
|
||||
self._tasks_lock = Lock()
|
||||
|
||||
# 处理其他kwargs参数
|
||||
@@ -183,7 +192,6 @@ class VirtualWorkbench:
|
||||
"""初始化虚拟工作台"""
|
||||
self.logger.info(f"初始化虚拟工作台 {self.device_id}")
|
||||
|
||||
# 重置加热台状态 (已在__init__中创建,这里重置为初始状态)
|
||||
with self._stations_lock:
|
||||
for station in self._heating_stations.values():
|
||||
station.state = HeatingStationState.IDLE
|
||||
@@ -191,7 +199,6 @@ class VirtualWorkbench:
|
||||
station.material_number = None
|
||||
station.heating_progress = 0.0
|
||||
|
||||
# 初始化状态
|
||||
self.data.update(
|
||||
{
|
||||
"status": "Ready",
|
||||
@@ -257,11 +264,7 @@ class VirtualWorkbench:
|
||||
self.data["message"] = message
|
||||
|
||||
def _find_available_heating_station(self) -> Optional[int]:
|
||||
"""查找空闲的加热台
|
||||
|
||||
Returns:
|
||||
空闲加热台ID,如果没有则返回None
|
||||
"""
|
||||
"""查找空闲的加热台"""
|
||||
with self._stations_lock:
|
||||
for station_id, station in self._heating_stations.items():
|
||||
if station.state == HeatingStationState.IDLE:
|
||||
@@ -269,23 +272,12 @@ class VirtualWorkbench:
|
||||
return None
|
||||
|
||||
def _acquire_arm(self, task_description: str) -> bool:
|
||||
"""获取机械臂使用权(阻塞直到获取)
|
||||
|
||||
Args:
|
||||
task_description: 任务描述,用于日志
|
||||
|
||||
Returns:
|
||||
是否成功获取
|
||||
"""
|
||||
"""获取机械臂使用权(阻塞直到获取)"""
|
||||
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
|
||||
|
||||
@@ -298,6 +290,22 @@ class VirtualWorkbench:
|
||||
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,
|
||||
@@ -306,19 +314,14 @@ class VirtualWorkbench:
|
||||
"""
|
||||
批量准备物料 - 虚拟起始节点
|
||||
|
||||
作为工作流的起始节点,生成指定数量的物料编号供后续节点使用。
|
||||
输出5个handle (material_1 ~ material_5),分别对应实验1~5。
|
||||
|
||||
Args:
|
||||
count: 待生成的物料数量,默认5 (生成 A1-A5)
|
||||
|
||||
Returns:
|
||||
PrepareMaterialsResult: 包含 material_1 ~ material_5 用于传递给 move_to_heating_station
|
||||
作为工作流的起始节点, 生成指定数量的物料编号供后续节点使用。
|
||||
输出5个handle (material_1 ~ material_5), 分别对应实验1~5。
|
||||
"""
|
||||
# 生成物料列表 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} 个物料: A1-A{count} -> material_1~material_{count}"
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
@@ -329,9 +332,28 @@ 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()]
|
||||
"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,
|
||||
@@ -340,20 +362,12 @@ class VirtualWorkbench:
|
||||
"""
|
||||
将物料从An位置移动到加热台
|
||||
|
||||
多线程并发调用时,会竞争机械臂使用权,并自动查找空闲加热台
|
||||
|
||||
Args:
|
||||
material_number: 物料编号 (1-5)
|
||||
|
||||
Returns:
|
||||
MoveToHeatingStationResult: 包含 station_id, material_number 等用于传递给下一个节点
|
||||
多线程并发调用时, 会竞争机械臂使用权, 并自动查找空闲加热台
|
||||
"""
|
||||
# 根据物料编号生成物料ID
|
||||
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",
|
||||
@@ -361,33 +375,27 @@ class VirtualWorkbench:
|
||||
}
|
||||
|
||||
try:
|
||||
# 步骤1: 等待获取机械臂使用权(竞争)
|
||||
with self._tasks_lock:
|
||||
self._active_tasks[material_id]["status"] = "waiting_for_arm"
|
||||
self._acquire_arm(task_desc)
|
||||
|
||||
# 步骤2: 查找空闲加热台
|
||||
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.logger.info(f"[{material_id}] 没有空闲加热台, 等待中...")
|
||||
self._release_arm()
|
||||
time.sleep(0.5)
|
||||
self._acquire_arm(task_desc)
|
||||
|
||||
# 步骤3: 占用加热台 - 立即标记为OCCUPIED,防止其他任务选择同一加热台
|
||||
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
|
||||
|
||||
# 步骤4: 模拟机械臂移动操作 (3秒)
|
||||
with self._tasks_lock:
|
||||
self._active_tasks[material_id]["status"] = "arm_moving"
|
||||
self._active_tasks[material_id]["assigned_station"] = station_id
|
||||
@@ -395,11 +403,11 @@ class VirtualWorkbench:
|
||||
|
||||
time.sleep(self.ARM_OPERATION_TIME)
|
||||
|
||||
# 步骤5: 放入加热台完成
|
||||
self._update_data_status(f"{material_id}已放入加热台{station_id}")
|
||||
self.logger.info(f"[{material_id}] 已放入加热台{station_id} (用时{self.ARM_OPERATION_TIME}s)")
|
||||
self.logger.info(
|
||||
f"[{material_id}] 已放入加热台{station_id} (用时{self.ARM_OPERATION_TIME}s)"
|
||||
)
|
||||
|
||||
# 释放机械臂
|
||||
self._release_arm()
|
||||
|
||||
with self._tasks_lock:
|
||||
@@ -412,8 +420,16 @@ class VirtualWorkbench:
|
||||
"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()]
|
||||
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:
|
||||
@@ -427,11 +443,33 @@ class VirtualWorkbench:
|
||||
"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()]
|
||||
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()
|
||||
],
|
||||
}
|
||||
|
||||
@always_free
|
||||
@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,
|
||||
@@ -440,13 +478,6 @@ class VirtualWorkbench:
|
||||
) -> StartHeatingResult:
|
||||
"""
|
||||
启动指定加热台的加热程序
|
||||
|
||||
Args:
|
||||
station_id: 加热台ID (1-3),从 move_to_heating_station 的 handle 传入
|
||||
material_number: 物料编号,从 move_to_heating_station 的 handle 传入
|
||||
|
||||
Returns:
|
||||
StartHeatingResult: 包含 station_id, material_number 等用于传递给下一个节点
|
||||
"""
|
||||
self.logger.info(f"[加热台{station_id}] 开始加热")
|
||||
|
||||
@@ -458,8 +489,16 @@ class VirtualWorkbench:
|
||||
"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()]
|
||||
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:
|
||||
@@ -473,8 +512,16 @@ class VirtualWorkbench:
|
||||
"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()]
|
||||
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:
|
||||
@@ -485,13 +532,20 @@ class VirtualWorkbench:
|
||||
"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()]
|
||||
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
|
||||
@@ -502,7 +556,6 @@ class VirtualWorkbench:
|
||||
|
||||
self._update_data_status(f"加热台{station_id}开始加热{material_id}")
|
||||
|
||||
# 打印当前所有正在加热的台位
|
||||
with self._stations_lock:
|
||||
heating_list = [
|
||||
f"加热台{sid}:{s.current_material}"
|
||||
@@ -511,7 +564,6 @@ class VirtualWorkbench:
|
||||
]
|
||||
self.logger.info(f"[并行加热] 当前同时加热中: {', '.join(heating_list)}")
|
||||
|
||||
# 模拟加热过程
|
||||
start_time = time.time()
|
||||
last_countdown_log = start_time
|
||||
while True:
|
||||
@@ -524,7 +576,6 @@ class VirtualWorkbench:
|
||||
|
||||
self._update_data_status(f"加热台{station_id}加热中: {progress:.1f}%")
|
||||
|
||||
# 每5秒打印一次倒计时
|
||||
if time.time() - last_countdown_log >= 5.0:
|
||||
self.logger.info(f"[加热台{station_id}] {material_id} 剩余 {remaining:.1f}s")
|
||||
last_countdown_log = time.time()
|
||||
@@ -534,7 +585,6 @@ class VirtualWorkbench:
|
||||
|
||||
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
|
||||
@@ -553,10 +603,28 @@ class VirtualWorkbench:
|
||||
"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()]
|
||||
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,
|
||||
@@ -565,15 +633,8 @@ class VirtualWorkbench:
|
||||
) -> MoveToOutputResult:
|
||||
"""
|
||||
将物料从加热台移动到输出位置Cn
|
||||
|
||||
Args:
|
||||
station_id: 加热台ID (1-3),从 start_heating 的 handle 传入
|
||||
material_number: 物料编号,从 start_heating 的 handle 传入,用于确定输出位置 Cn
|
||||
|
||||
Returns:
|
||||
MoveToOutputResult: 包含执行结果
|
||||
"""
|
||||
output_number = material_number # 物料编号决定输出位置
|
||||
output_number = material_number
|
||||
|
||||
if station_id not in self._heating_stations:
|
||||
return {
|
||||
@@ -583,8 +644,16 @@ class VirtualWorkbench:
|
||||
"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()]
|
||||
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:
|
||||
@@ -599,8 +668,16 @@ class VirtualWorkbench:
|
||||
"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()]
|
||||
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:
|
||||
@@ -611,8 +688,16 @@ class VirtualWorkbench:
|
||||
"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()]
|
||||
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}"
|
||||
@@ -624,18 +709,17 @@ class VirtualWorkbench:
|
||||
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"
|
||||
|
||||
# 模拟机械臂操作 (3秒)
|
||||
self.logger.info(f"[{material_id}] 机械臂正在从加热台{station_id}取出并移动到{output_position}...")
|
||||
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
|
||||
@@ -643,17 +727,17 @@ class VirtualWorkbench:
|
||||
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)")
|
||||
self.logger.info(
|
||||
f"[{material_id}] 已成功移动到{output_position} (用时{self.ARM_OPERATION_TIME}s)"
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
@@ -662,8 +746,17 @@ class VirtualWorkbench:
|
||||
"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()]
|
||||
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:
|
||||
@@ -677,83 +770,105 @@ class VirtualWorkbench:
|
||||
"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()]
|
||||
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", "")
|
||||
|
||||
Reference in New Issue
Block a user