mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-03-25 10:15:45 +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
|
self._ros_node = workstation_node
|
||||||
logger.info(f"工作站 {self._ros_node.device_id} 关联协议节点")
|
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:
|
def call_device_method(self, method: str, *args, **kwargs) -> Any:
|
||||||
|
|||||||
@@ -217,6 +217,24 @@ class AGVTransferProtocol(BaseModel):
|
|||||||
from_repo_position: str
|
from_repo_position: str
|
||||||
to_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):
|
class AddProtocol(BaseModel):
|
||||||
vessel: dict
|
vessel: dict
|
||||||
@@ -629,15 +647,16 @@ class HydrogenateProtocol(BaseModel):
|
|||||||
vessel: dict = Field(..., description="反应容器")
|
vessel: dict = Field(..., description="反应容器")
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Point3D", "PumpTransferProtocol", "CleanProtocol", "SeparateProtocol",
|
"Point3D", "PumpTransferProtocol", "CleanProtocol", "SeparateProtocol",
|
||||||
"EvaporateProtocol", "EvacuateAndRefillProtocol", "AGVTransferProtocol",
|
"EvaporateProtocol", "EvacuateAndRefillProtocol", "AGVTransferProtocol",
|
||||||
"CentrifugeProtocol", "AddProtocol", "FilterProtocol",
|
"BatchTransferItem", "BatchTransferProtocol",
|
||||||
|
"CentrifugeProtocol", "AddProtocol", "FilterProtocol",
|
||||||
"HeatChillProtocol",
|
"HeatChillProtocol",
|
||||||
"HeatChillStartProtocol", "HeatChillStopProtocol",
|
"HeatChillStartProtocol", "HeatChillStopProtocol",
|
||||||
"StirProtocol", "StartStirProtocol", "StopStirProtocol",
|
"StirProtocol", "StartStirProtocol", "StopStirProtocol",
|
||||||
"TransferProtocol", "CleanVesselProtocol", "DissolveProtocol",
|
"TransferProtocol", "CleanVesselProtocol", "DissolveProtocol",
|
||||||
"FilterThroughProtocol", "RunColumnProtocol", "WashSolidProtocol",
|
"FilterThroughProtocol", "RunColumnProtocol", "WashSolidProtocol",
|
||||||
"AdjustPHProtocol", "ResetHandlingProtocol", "DryProtocol",
|
"AdjustPHProtocol", "ResetHandlingProtocol", "DryProtocol",
|
||||||
"RecrystallizeProtocol", "HydrogenateProtocol"
|
"RecrystallizeProtocol", "HydrogenateProtocol"
|
||||||
]
|
]
|
||||||
# End Protocols
|
# End Protocols
|
||||||
|
|||||||
@@ -142,12 +142,16 @@ _msg_converter: Dict[Type, Any] = {
|
|||||||
),
|
),
|
||||||
Resource: lambda x: Resource(
|
Resource: lambda x: Resource(
|
||||||
id=x.get("id", ""),
|
id=x.get("id", ""),
|
||||||
|
uuid=x.get("uuid", ""),
|
||||||
name=x.get("name", ""),
|
name=x.get("name", ""),
|
||||||
sample_id=x.get("sample_id", "") or "",
|
sample_id=x.get("sample_id", "") or "",
|
||||||
|
description=x.get("description", ""),
|
||||||
children=list(x.get("children", [])),
|
children=list(x.get("children", [])),
|
||||||
parent=x.get("parent", "") or "",
|
parent=x.get("parent", "") or "",
|
||||||
|
parent_uuid=x.get("parent_uuid", ""),
|
||||||
type=x.get("type", ""),
|
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=(
|
||||||
Pose(
|
Pose(
|
||||||
position=Point(
|
position=Point(
|
||||||
@@ -160,15 +164,11 @@ _msg_converter: Dict[Type, Any] = {
|
|||||||
else Pose()
|
else Pose()
|
||||||
),
|
),
|
||||||
config=json.dumps(x.get("config", {})),
|
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:
|
def json_or_yaml_loads(data: str) -> Any:
|
||||||
try:
|
try:
|
||||||
return json.loads(data)
|
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),
|
Point: lambda x: Point3D(x=x.x, y=x.y, z=x.z),
|
||||||
Resource: lambda x: {
|
Resource: lambda x: {
|
||||||
"id": x.id,
|
"id": x.id,
|
||||||
|
"uuid": x.uuid if x.uuid else None,
|
||||||
"name": x.name,
|
"name": x.name,
|
||||||
"sample_id": x.sample_id if x.sample_id else None,
|
"sample_id": x.sample_id if x.sample_id else None,
|
||||||
|
"description": x.description if x.description else "",
|
||||||
"children": list(x.children),
|
"children": list(x.children),
|
||||||
"parent": x.parent if x.parent else None,
|
"parent": x.parent if x.parent else None,
|
||||||
|
"parent_uuid": x.parent_uuid if x.parent_uuid else None,
|
||||||
"type": x.type,
|
"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},
|
"position": {"x": x.pose.position.x, "y": x.pose.position.y, "z": x.pose.position.z},
|
||||||
"config": json_or_yaml_loads(x.config or "{}"),
|
"config": json_or_yaml_loads(x.config or "{}"),
|
||||||
"data": json_or_yaml_loads(x.data 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()}"
|
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):
|
async def s2c_resource_tree(self, req: SerialCommand_Request, res: SerialCommand_Response):
|
||||||
"""
|
"""
|
||||||
处理资源树更新请求
|
处理资源树更新请求
|
||||||
|
|||||||
@@ -340,8 +340,17 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
|
|||||||
) # type: ignore
|
) # type: ignore
|
||||||
raw_data = json.loads(response.response)
|
raw_data = json.loads(response.response)
|
||||||
tree_set = ResourceTreeSet.from_raw_dict_list(raw_data)
|
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:
|
except Exception as ex:
|
||||||
self.lab_logger().error(f"查询资源失败: {k}, 错误: {ex}\n{traceback.format_exc()}")
|
self.lab_logger().error(f"查询资源失败: {k}, 错误: {ex}\n{traceback.format_exc()}")
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
string id
|
string id
|
||||||
|
string uuid
|
||||||
string name
|
string name
|
||||||
string sample_id
|
string sample_id
|
||||||
|
string description
|
||||||
string[] children
|
string[] children
|
||||||
string parent
|
string parent
|
||||||
|
string parent_uuid
|
||||||
string type
|
string type
|
||||||
string category
|
string category
|
||||||
|
string klass
|
||||||
|
|
||||||
geometry_msgs/Pose pose
|
geometry_msgs/Pose pose
|
||||||
string config
|
string config
|
||||||
string data
|
string data
|
||||||
|
string extra
|
||||||
|
|||||||
Reference in New Issue
Block a user