mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-03-25 08:25:13 +00:00
feat: 升级Resource消息系统,增加uuid和klass字段
Resource.msg新增uuid和klass字段支持ResourceDictInstance完整序列化, message_converter增加Resource消息与Python dict的双向转换, workstation和base_device_node增加资源同步相关功能。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -197,6 +197,28 @@ class WorkstationBase(ABC):
|
||||
self._ros_node = workstation_node
|
||||
logger.info(f"工作站 {self._ros_node.device_id} 关联协议节点")
|
||||
|
||||
# ============ 物料转运回调 ============
|
||||
|
||||
def resource_tree_batch_transfer(
|
||||
self,
|
||||
transfers: list,
|
||||
old_parents: list,
|
||||
new_parents: list,
|
||||
) -> None:
|
||||
"""批量物料转运完成后的回调,供子类重写
|
||||
|
||||
默认实现:逐个调用 resource_tree_transfer(如存在)。
|
||||
|
||||
Args:
|
||||
transfers: 转移列表,每项包含 resource, from_parent, to_parent, to_site 等
|
||||
old_parents: 每个物料转移前的原父节点
|
||||
new_parents: 每个物料转移后的新父节点
|
||||
"""
|
||||
func = getattr(self, "resource_tree_transfer", None)
|
||||
if callable(func):
|
||||
for t, old_parent, new_parent in zip(transfers, old_parents, new_parents):
|
||||
func(old_parent, t["resource"], new_parent)
|
||||
|
||||
# ============ 设备操作接口 ============
|
||||
|
||||
def call_device_method(self, method: str, *args, **kwargs) -> Any:
|
||||
|
||||
@@ -217,6 +217,24 @@ class AGVTransferProtocol(BaseModel):
|
||||
from_repo_position: str
|
||||
to_repo_position: str
|
||||
|
||||
|
||||
class BatchTransferItem(BaseModel):
|
||||
"""批量转运中的单个物料条目"""
|
||||
resource_uuid: str = ""
|
||||
resource_id: str = ""
|
||||
from_position: str
|
||||
to_position: str
|
||||
|
||||
|
||||
class BatchTransferProtocol(BaseModel):
|
||||
"""批量物料转运协议 — 支持多物料一次性从来源工站转运到目标工站"""
|
||||
from_repo: dict
|
||||
to_repo: dict
|
||||
transfer_resources: list # list[Resource dict],被转运的物料
|
||||
from_positions: list # list[str],来源 slot 位置(与 transfer_resources 平行)
|
||||
to_positions: list # list[str],目标 slot 位置(与 transfer_resources 平行)
|
||||
|
||||
|
||||
#=============新添加的新的协议================
|
||||
class AddProtocol(BaseModel):
|
||||
vessel: dict
|
||||
@@ -629,15 +647,16 @@ class HydrogenateProtocol(BaseModel):
|
||||
vessel: dict = Field(..., description="反应容器")
|
||||
|
||||
__all__ = [
|
||||
"Point3D", "PumpTransferProtocol", "CleanProtocol", "SeparateProtocol",
|
||||
"EvaporateProtocol", "EvacuateAndRefillProtocol", "AGVTransferProtocol",
|
||||
"CentrifugeProtocol", "AddProtocol", "FilterProtocol",
|
||||
"Point3D", "PumpTransferProtocol", "CleanProtocol", "SeparateProtocol",
|
||||
"EvaporateProtocol", "EvacuateAndRefillProtocol", "AGVTransferProtocol",
|
||||
"BatchTransferItem", "BatchTransferProtocol",
|
||||
"CentrifugeProtocol", "AddProtocol", "FilterProtocol",
|
||||
"HeatChillProtocol",
|
||||
"HeatChillStartProtocol", "HeatChillStopProtocol",
|
||||
"StirProtocol", "StartStirProtocol", "StopStirProtocol",
|
||||
"TransferProtocol", "CleanVesselProtocol", "DissolveProtocol",
|
||||
"StirProtocol", "StartStirProtocol", "StopStirProtocol",
|
||||
"TransferProtocol", "CleanVesselProtocol", "DissolveProtocol",
|
||||
"FilterThroughProtocol", "RunColumnProtocol", "WashSolidProtocol",
|
||||
"AdjustPHProtocol", "ResetHandlingProtocol", "DryProtocol",
|
||||
"AdjustPHProtocol", "ResetHandlingProtocol", "DryProtocol",
|
||||
"RecrystallizeProtocol", "HydrogenateProtocol"
|
||||
]
|
||||
# End Protocols
|
||||
|
||||
@@ -142,12 +142,16 @@ _msg_converter: Dict[Type, Any] = {
|
||||
),
|
||||
Resource: lambda x: Resource(
|
||||
id=x.get("id", ""),
|
||||
uuid=x.get("uuid", ""),
|
||||
name=x.get("name", ""),
|
||||
sample_id=x.get("sample_id", "") or "",
|
||||
description=x.get("description", ""),
|
||||
children=list(x.get("children", [])),
|
||||
parent=x.get("parent", "") or "",
|
||||
parent_uuid=x.get("parent_uuid", ""),
|
||||
type=x.get("type", ""),
|
||||
category=x.get("class", "") or x.get("type", ""),
|
||||
category=x.get("category", "") or x.get("class", "") or x.get("type", ""),
|
||||
klass=x.get("class", "") or x.get("klass", ""),
|
||||
pose=(
|
||||
Pose(
|
||||
position=Point(
|
||||
@@ -160,15 +164,11 @@ _msg_converter: Dict[Type, Any] = {
|
||||
else Pose()
|
||||
),
|
||||
config=json.dumps(x.get("config", {})),
|
||||
data=json.dumps(obtain_data_with_uuid(x)),
|
||||
data=json.dumps(x.get("data", {})),
|
||||
extra=json.dumps(x.get("extra", {})),
|
||||
),
|
||||
}
|
||||
|
||||
def obtain_data_with_uuid(x: dict):
|
||||
data = x.get("data", {})
|
||||
data["unilabos_uuid"] = x.get("uuid", None)
|
||||
return data
|
||||
|
||||
def json_or_yaml_loads(data: str) -> Any:
|
||||
try:
|
||||
return json.loads(data)
|
||||
@@ -195,15 +195,20 @@ _msg_converter_back: Dict[Type, Any] = {
|
||||
Point: lambda x: Point3D(x=x.x, y=x.y, z=x.z),
|
||||
Resource: lambda x: {
|
||||
"id": x.id,
|
||||
"uuid": x.uuid if x.uuid else None,
|
||||
"name": x.name,
|
||||
"sample_id": x.sample_id if x.sample_id else None,
|
||||
"description": x.description if x.description else "",
|
||||
"children": list(x.children),
|
||||
"parent": x.parent if x.parent else None,
|
||||
"parent_uuid": x.parent_uuid if x.parent_uuid else None,
|
||||
"type": x.type,
|
||||
"class": "",
|
||||
"class": x.klass if x.klass else "",
|
||||
"category": x.category if x.category else "",
|
||||
"position": {"x": x.pose.position.x, "y": x.pose.position.y, "z": x.pose.position.z},
|
||||
"config": json_or_yaml_loads(x.config or "{}"),
|
||||
"data": json_or_yaml_loads(x.data or "{}"),
|
||||
"extra": json_or_yaml_loads(x.extra or "{}"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -761,6 +761,84 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_resource}[{parent_uuid}]失败!\n{traceback.format_exc()}"
|
||||
)
|
||||
|
||||
def batch_transfer_resources(
|
||||
self,
|
||||
transfers: List[Dict[str, Any]],
|
||||
) -> List["ResourcePLR"]:
|
||||
"""批量转移 PLR 资源:先全部 unassign,再全部 assign,最后一次性回调和同步
|
||||
|
||||
Args:
|
||||
transfers: 转移列表,每项包含:
|
||||
- "resource": PLR 资源对象
|
||||
- "from_parent": 原父节点 (PLR ResourceHolder/WareHouse)
|
||||
- "to_parent": 目标父节点
|
||||
- "to_site": 目标 slot 名称(可选,用于 ItemizedCarrier)
|
||||
|
||||
Returns:
|
||||
成功转移的目标 parent 列表
|
||||
"""
|
||||
from pylabrobot.resources.resource import Resource as ResourcePLR
|
||||
|
||||
if not transfers:
|
||||
return []
|
||||
|
||||
# 第一遍:校验所有物料和目标 parent 的合法性
|
||||
for t in transfers:
|
||||
resource = t["resource"]
|
||||
to_parent = t["to_parent"]
|
||||
if resource is None:
|
||||
raise ValueError("转移列表中存在 resource=None")
|
||||
if to_parent is None:
|
||||
raise ValueError(f"物料 {resource} 的目标 parent 为 None")
|
||||
|
||||
# 第二遍:批量 unassign
|
||||
old_parents = []
|
||||
for t in transfers:
|
||||
resource = t["resource"]
|
||||
old_parent = resource.parent
|
||||
old_parents.append(old_parent)
|
||||
if old_parent is not None:
|
||||
self.lab_logger().debug(f"批量 unassign: {resource.name} 从 {old_parent.name}")
|
||||
old_parent.unassign_child_resource(resource)
|
||||
# 从顶级资源列表中移除(避免 figure_resource 重复引用)
|
||||
resource_id = id(resource)
|
||||
for i, r in enumerate(self.resource_tracker.resources):
|
||||
if id(r) == resource_id:
|
||||
self.resource_tracker.resources.pop(i)
|
||||
break
|
||||
|
||||
# 第三遍:批量 assign
|
||||
parents = []
|
||||
for t, old_parent in zip(transfers, old_parents):
|
||||
resource = t["resource"]
|
||||
to_parent = t["to_parent"]
|
||||
to_site = t.get("to_site")
|
||||
additional_params = {}
|
||||
if to_site is not None:
|
||||
spec = inspect.signature(to_parent.assign_child_resource)
|
||||
if "spot" in spec.parameters:
|
||||
ordering_dict = getattr(to_parent, "_ordering", None)
|
||||
if ordering_dict and to_site in ordering_dict:
|
||||
additional_params["spot"] = list(ordering_dict.keys()).index(to_site)
|
||||
else:
|
||||
additional_params["spot"] = to_site
|
||||
self.lab_logger().debug(f"批量 assign: {resource.name} → {to_parent.name} site={to_site}")
|
||||
to_parent.assign_child_resource(resource, location=None, **additional_params)
|
||||
parents.append(to_parent)
|
||||
|
||||
# 一次性触发 driver 回调
|
||||
func = getattr(self.driver_instance, "resource_tree_batch_transfer", None)
|
||||
if callable(func):
|
||||
func(transfers, old_parents, parents)
|
||||
else:
|
||||
# 兜底:逐个调用 resource_tree_transfer
|
||||
single_func = getattr(self.driver_instance, "resource_tree_transfer", None)
|
||||
if callable(single_func):
|
||||
for t, old_parent, new_parent in zip(transfers, old_parents, parents):
|
||||
single_func(old_parent, t["resource"], new_parent)
|
||||
|
||||
return parents
|
||||
|
||||
async def s2c_resource_tree(self, req: SerialCommand_Request, res: SerialCommand_Response):
|
||||
"""
|
||||
处理资源树更新请求
|
||||
|
||||
@@ -340,8 +340,17 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
|
||||
) # type: ignore
|
||||
raw_data = json.loads(response.response)
|
||||
tree_set = ResourceTreeSet.from_raw_dict_list(raw_data)
|
||||
target = tree_set.dump()
|
||||
protocol_kwargs[k] = target[0][0] if v == "unilabos_msgs/Resource" else target
|
||||
|
||||
# 传递 ResourceDictInstance(保留树结构),不再调用 dump() 扁平化
|
||||
if v == "unilabos_msgs/Resource":
|
||||
# 单个资源:取第一棵树的根节点
|
||||
root_instance = tree_set.trees[0].root_node if tree_set.trees else None
|
||||
protocol_kwargs[k] = root_instance.get_plr_nested_dict() if root_instance else protocol_kwargs[k]
|
||||
else:
|
||||
# 多个资源:取每棵树的根节点
|
||||
protocol_kwargs[k] = [
|
||||
tree.root_node.get_plr_nested_dict() for tree in tree_set.trees
|
||||
]
|
||||
except Exception as ex:
|
||||
self.lab_logger().error(f"查询资源失败: {k}, 错误: {ex}\n{traceback.format_exc()}")
|
||||
raise
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
string id
|
||||
string uuid
|
||||
string name
|
||||
string sample_id
|
||||
string description
|
||||
string[] children
|
||||
string parent
|
||||
string parent_uuid
|
||||
string type
|
||||
string category
|
||||
string klass
|
||||
|
||||
geometry_msgs/Pose pose
|
||||
string config
|
||||
string data
|
||||
string data
|
||||
string extra
|
||||
|
||||
Reference in New Issue
Block a user