add update remove

This commit is contained in:
Xuwznln
2025-10-10 20:15:16 +08:00
parent a1783f489e
commit 67c01271b7
4 changed files with 267 additions and 112 deletions

View File

@@ -107,6 +107,8 @@ class HTTPClient:
for u, n in old_uuids.items():
if u in uuid_mapping:
n.res_content.uuid = uuid_mapping[u]
for c in n.children:
c.res_content.parent_uuid = n.res_content.uuid
else:
logger.warning(f"资源UUID未更新: {u}")
return uuid_mapping

View File

@@ -386,13 +386,33 @@ class BaseROS2DeviceNode(Node, Generic[T]):
try:
if action == "add":
# 添加资源到资源跟踪器
plr_resource = tree_set.to_plr_resources() # FIXME: 转成plr的实例
plr_resources = tree_set.to_plr_resources()
for plr_resource, tree in zip(plr_resources, tree_set.trees):
self.resource_tracker.add_resource(plr_resource)
parent_uuid = tree.root_node.res_content.parent_uuid
if parent_uuid:
parent_resource: ResourcePLR = self.resource_tracker.uuid_to_resources.get(parent_uuid)
if parent_resource is None:
self.lab_logger().warning(f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_uuid}不存在")
else:
try:
parent_resource.assign_child_resource(plr_resource, location=None)
except Exception as e:
self.lab_logger().warning(
f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_resource}[{parent_uuid}]失败!\n{traceback.format_exc()}")
func = getattr(self.driver_instance, "resource_tree_add", None)
if callable(func):
func(tree_set)
func(plr_resources)
results.append({"success": True, "action": "add"})
elif action == "update":
# 更新资源
plr_resources = tree_set.to_plr_resources()
for plr_resource, tree in zip(plr_resources, tree_set.trees):
states = plr_resource.serialize_all_state()
original_instance: ResourcePLR = self.resource_tracker.figure_resource({"uuid": tree.root_node.res_content.uuid}, try_mode=False)
original_instance.load_all_state(states)
self.lab_logger().info(f"更新了资源属性 {plr_resource}[{tree.root_node.res_content.uuid}] 及其子节点 {len(original_instance.get_all_children())}")
func = getattr(self.driver_instance, "resource_tree_update", None)
if callable(func):
func(tree_set)
@@ -401,10 +421,12 @@ class BaseROS2DeviceNode(Node, Generic[T]):
# 移除资源
func = getattr(self.driver_instance, "resource_tree_remove", None)
if callable(func):
resources_instance: List[ResourcePLR] = [self.resource_tracker.uuid_to_resources[i] for
resources_instances: List[ResourcePLR] = [self.resource_tracker.uuid_to_resources[i] for
i in resources_uuid]
func(resources_instance)
[r.parent.unassign_child_resource(r) for r in resources_instance if r is not None]
self.resource_tracker.add_resource()
[r.parent.unassign_child_resource(r) for r in resources_instances if r is not None]
func(resources_instances)
[r.parent.unassign_child_resource(r) for r in resources_instances if r is not None]
results.append({"success": True, "action": "remove"})
except Exception as e:
error_msg = f"Error processing {action} operation: {str(e)}"

View File

@@ -175,9 +175,6 @@ class HostNode(BaseROS2DeviceNode):
for device_config in devices_config.root_nodes:
device_id = device_config.res_content.id
if device_config.res_content.type != "device":
self.lab_logger().debug(
f"[Host Node] Skipping type {device_config.res_content.type} {device_id} already existed, skipping."
)
continue
if device_id not in self.devices_names:
self.initialize_device(device_id, device_config)

View File

@@ -52,18 +52,17 @@ class ResourceDict(BaseModel):
schema: Dict[str, Any] = Field(description="Resource schema", default_factory=dict)
model: Dict[str, Any] = Field(description="Resource model", default_factory=dict)
icon: str = Field(description="Resource icon", default="")
parent: Optional["ResourceDict"] = Field(
description="Parent resource object", default=None, serialization_alias="parent_uuid"
)
parent_uuid: Optional["str"] = Field(description="Parent resource uuid", default=None) # 先设定parent_uuid
parent: Optional["ResourceDict"] = Field(description="Parent resource object", default=None, exclude=True)
type: Literal["device"] | str = Field(description="Resource type")
klass: str = Field(alias="class", description="Resource class name")
position: ResourceDictPosition = Field(description="Resource position", default_factory=ResourceDictPosition)
config: Dict[str, Any] = Field(description="Resource configuration")
data: Dict[str, Any] = Field(description="Resource data")
@field_serializer("parent")
def _serialize_parent(self, parent: Optional["ResourceDict"]):
return self.parent_uuid
@field_serializer("parent_uuid")
def _serialize_parent(self, parent_uuid: Optional["ResourceDict"]):
return self.uuid_parent
@field_validator("parent", mode="before")
@classmethod
@@ -74,13 +73,23 @@ class ResourceDict(BaseModel):
return None
@property
def parent_uuid(self) -> str:
def uuid_parent(self) -> str:
"""获取父节点的UUID"""
return self.parent.uuid if self.parent is not None else ""
parent_instance_uuid = self.parent_instance_uuid
if parent_instance_uuid is not None and self.parent_uuid and parent_instance_uuid != self.parent_uuid:
logger.warning(f"{self.name}[{self.uuid}]的parent uuid未同步") # 现在强制要求设置
if parent_instance_uuid is not None:
return parent_instance_uuid
return self.parent_uuid
@property
def parent_name(self) -> Optional[str]:
def parent_instance_uuid(self) -> Optional[str]:
"""获取父节点的UUID"""
return self.parent.uuid if self.parent is not None else None
@property
def parent_instance_name(self) -> Optional[str]:
"""获取父节点的名字"""
return self.parent.name if self.parent is not None else None
@property
@@ -101,7 +110,7 @@ class ResourceDictInstance(object):
def __init__(self, res_content: "ResourceDict"):
self.res_content = res_content
self.children = []
self.children: List[ResourceDictInstance] = []
self.typ = "dict"
@classmethod
@@ -132,7 +141,7 @@ class ResourceDictInstance(object):
"""获取资源实例的嵌套字典表示"""
res_dict = self.res_content.model_dump(by_alias=True)
res_dict["children"] = {child.res_content.name: child.get_nested_dict() for child in self.children}
res_dict["parent"] = self.res_content.parent_name
res_dict["parent"] = self.res_content.parent_instance_name
res_dict["position"] = self.res_content.position.position.model_dump()
return res_dict
@@ -174,7 +183,7 @@ class ResourceTreeInstance(object):
current_uuid = queue.pop(0)
# 查找所有parent_uuid指向当前节点的子节点
for uuid_str, res in uuid_map.items():
if res.parent_uuid == current_uuid and uuid_str not in visited:
if res.uuid_parent == current_uuid and uuid_str not in visited:
child_instance = ResourceDictInstance(res)
tree_nodes.append(child_instance)
visited.add(uuid_str)
@@ -363,14 +372,12 @@ class ResourceTreeSet(object):
trees.append(tree_instance)
return cls(trees)
def to_plr_resources(self) -> Tuple[List["PLRResource"], List[Dict[str, str]]]:
def to_plr_resources(self) -> List["PLRResource"]:
"""
将 ResourceTreeSet 转换为 PLR 资源列表
Returns:
Tuple[List[PLRResource], List[Dict[str, str]]]:
- PLR 资源实例列表
- 每个资源对应的 name_to_uuid 映射字典列表
List[PLRResource]: PLR 资源实例列表
"""
from pylabrobot.resources import Resource as PLRResource
from pylabrobot.utils.object_parsing import find_subclass
@@ -408,7 +415,7 @@ class ResourceTreeSet(object):
"rotation": {"x": 0, "y": 0, "z": 0, "type": "Rotation"},
"category": plr_type,
"children": [node_to_plr_dict(child, has_model) for child in node.children],
"parent_name": res.parent_name,
"parent_name": res.parent_instance_name,
**res.config,
}
if has_model:
@@ -416,34 +423,27 @@ class ResourceTreeSet(object):
return d
plr_resources = []
name_to_uuid_maps = []
trees = []
tracker = DeviceNodeResourceTracker()
for tree in self.trees:
name_to_uuid: Dict[str, str] = {}
all_states: Dict[str, Any] = {}
collect_node_data(tree.root_node, name_to_uuid, all_states)
has_model = tree.root_node.res_content.type != "deck"
plr_dict = node_to_plr_dict(tree.root_node, has_model)
try:
sub_cls = find_subclass(plr_dict["type"], PLRResource)
if sub_cls is None:
raise ValueError(f"无法找到类型 {plr_dict['type']} 对应的 PLR 资源类")
spec = inspect.signature(sub_cls)
if "category" not in spec.parameters:
plr_dict.pop("category", None)
plr_resource = sub_cls.deserialize(plr_dict, allow_marshal=True)
plr_resource.load_all_state(all_states)
# 使用 DeviceNodeResourceTracker 设置 UUID
tracker.loop_set_uuid(plr_resource, name_to_uuid)
plr_resources.append(plr_resource)
name_to_uuid_maps.append(name_to_uuid)
except Exception as e:
logger.error(f"转换 PLR 资源失败: {e}")
@@ -452,7 +452,7 @@ class ResourceTreeSet(object):
logger.error(f"堆栈: {traceback.format_exc()}")
raise
return plr_resources, name_to_uuid_maps
return plr_resources
@classmethod
def from_nested_list(cls, nested_list: List[ResourceDictInstance]) -> "ResourceTreeSet":
@@ -473,7 +473,7 @@ class ResourceTreeSet(object):
root_instances = [
ResourceTreeInstance(res_instance)
for res_instance in nested_list
if res_instance.res_content.is_root_node or res_instance.res_content.parent_uuid not in known_uuids
if res_instance.res_content.is_root_node or res_instance.res_content.uuid_parent not in known_uuids
]
return cls(root_instances)
@@ -584,6 +584,63 @@ class DeviceNodeResourceTracker(object):
self.uuid_to_resources[new_uuid] = instance
print(f"更新uuid映射: {old_uuid} -> {new_uuid} | {instance}")
def _get_resource_attr(self, resource, attr_name: str, uuid_attr: Optional[str] = None):
"""
获取资源的属性值,统一处理 dict 和 instance 两种类型
Args:
resource: 资源对象dict或实例
attr_name: dict类型使用的属性名
uuid_attr: instance类型使用的属性名用于uuid字段默认与attr_name相同
Returns:
属性值不存在则返回None
"""
if uuid_attr is None:
uuid_attr = attr_name
if isinstance(resource, dict):
return resource.get(attr_name)
else:
return getattr(resource, uuid_attr, None)
def _set_resource_uuid(self, resource, new_uuid: str):
"""
设置资源的 uuid统一处理 dict 和 instance 两种类型
Args:
resource: 资源对象dict或实例
new_uuid: 新的uuid值
"""
if isinstance(resource, dict):
resource["uuid"] = new_uuid
else:
setattr(resource, "unilabos_uuid", new_uuid)
def _traverse_and_process(self, resource, process_func) -> int:
"""
递归遍历资源树,对每个节点执行处理函数
Args:
resource: 资源对象可以是list、dict或实例
process_func: 处理函数接收resource参数返回处理的节点数量
Returns:
处理的节点总数量
"""
if isinstance(resource, list):
return sum(self._traverse_and_process(r, process_func) for r in resource)
# 先递归处理所有子节点
count = 0
children = getattr(resource, "children", [])
for child in children:
count += self._traverse_and_process(child, process_func)
# 处理当前节点
count += process_func(resource)
return count
def loop_set_uuid(self, resource, name_to_uuid_map: Dict[str, str]) -> int:
"""
递归遍历资源树,根据 name 设置所有节点的 uuid
@@ -595,36 +652,18 @@ class DeviceNodeResourceTracker(object):
Returns:
更新的资源数量
"""
if isinstance(resource, list):
return sum(self.loop_set_uuid(r, name_to_uuid_map) for r in resource)
update_count = 0
def process(res):
resource_name = self._get_resource_attr(res, "name")
if resource_name and resource_name in name_to_uuid_map:
new_uuid = name_to_uuid_map[resource_name]
self._set_resource_uuid(res, new_uuid)
self.uuid_to_resources[new_uuid] = res
logger.debug(f"设置资源UUID: {resource_name} -> {new_uuid}")
return 1
return 0
# 先递归处理所有子节点
children = getattr(resource, "children", [])
for child in children:
update_count += self.loop_set_uuid(child, name_to_uuid_map)
# 获取当前资源的name
if isinstance(resource, dict):
resource_name = resource.get("name")
else:
resource_name = getattr(resource, "name", None)
# 如果name在映射中则设置uuid
if resource_name and resource_name in name_to_uuid_map:
new_uuid = name_to_uuid_map[resource_name]
# 更新资源的uuid
if isinstance(resource, dict):
resource["uuid"] = new_uuid
else:
# 对于PLR资源设置unilabos_uuid
setattr(resource, "unilabos_uuid", new_uuid)
self.uuid_to_resources[new_uuid] = resource
update_count += 1
logger.debug(f"设置资源UUID: {resource_name} -> {new_uuid}")
return update_count
return self._traverse_and_process(resource, process)
def loop_update_uuid(self, resource, uuid_map: Dict[str, str]) -> int:
"""
@@ -637,43 +676,56 @@ class DeviceNodeResourceTracker(object):
Returns:
更新的资源数量
"""
if isinstance(resource, list):
return sum(self.loop_update_uuid(r, uuid_map) for r in resource)
update_count = 0
def process(res):
current_uuid = self._get_resource_attr(res, "uuid", "unilabos_uuid")
if current_uuid and current_uuid in uuid_map:
new_uuid = uuid_map[current_uuid]
if current_uuid != new_uuid:
self._set_resource_uuid(res, new_uuid)
# 更新uuid_to_resources映射
if current_uuid in self.uuid_to_resources:
self.uuid_to_resources.pop(current_uuid)
self.uuid_to_resources[new_uuid] = res
logger.debug(f"更新uuid: {current_uuid} -> {new_uuid}")
return 1
return 0
# 先递归处理所有子节点
children = getattr(resource, "children", [])
for child in children:
update_count += self.loop_update_uuid(child, uuid_map)
return self._traverse_and_process(resource, process)
# 获取当前资源的uuid
if isinstance(resource, dict):
current_uuid = resource.get("uuid")
else:
current_uuid = getattr(resource, "unilabos_uuid", None)
def _collect_uuid_mapping(self, resource):
"""
递归收集资源的 uuid 映射到 uuid_to_resources
# 如果当前uuid在映射中则更新
if current_uuid and current_uuid in uuid_map:
new_uuid = uuid_map[current_uuid]
if current_uuid != new_uuid:
# 更新资源的uuid
if isinstance(resource, dict):
resource["uuid"] = new_uuid
else:
# 对于PLR资源更新unilabos_uuid
if hasattr(resource, "unilabos_uuid"):
setattr(resource, "unilabos_uuid", new_uuid)
Args:
resource: 资源对象可以是dict或实例
"""
# 更新uuid_to_resources映射
if current_uuid in self.uuid_to_resources:
instance = self.uuid_to_resources.pop(current_uuid)
self.uuid_to_resources[new_uuid] = instance
def process(res):
current_uuid = self._get_resource_attr(res, "uuid", "unilabos_uuid")
if current_uuid:
self.uuid_to_resources[current_uuid] = res
logger.debug(f"收集资源UUID映射: {current_uuid} -> {res}")
return 0
update_count += 1
logger.debug(f"更新uuid: {current_uuid} -> {new_uuid} | {resource}")
self._traverse_and_process(resource, process)
return update_count
def _remove_uuid_mapping(self, resource):
"""
递归清除资源的 uuid 映射
Args:
resource: 资源对象可以是dict或实例
"""
def process(res):
current_uuid = self._get_resource_attr(res, "uuid", "unilabos_uuid")
if current_uuid and current_uuid in self.uuid_to_resources:
self.uuid_to_resources.pop(current_uuid)
logger.debug(f"移除资源UUID映射: {current_uuid} -> {res}")
return 0
self._traverse_and_process(resource, process)
def parent_resource(self, resource):
if id(resource) in self.resource2parent_resource:
@@ -682,21 +734,105 @@ class DeviceNodeResourceTracker(object):
return resource
def add_resource(self, resource):
"""
添加资源到追踪器
Args:
resource: 资源对象可以是dict或实例
"""
for r in self.resources:
if id(r) == id(resource):
return
self.resources.append(resource)
# 递归收集uuid映射
self._collect_uuid_mapping(resource)
def remove_resource(self, resource) -> bool:
"""
从追踪器中移除资源
Args:
resource: 资源对象可以是dict或实例
Returns:
bool: 如果成功移除返回True资源不存在返回False
"""
# 从 resources 列表中移除
resource_id = id(resource)
removed = False
for i, r in enumerate(self.resources):
if id(r) == resource_id:
self.resources.pop(i)
removed = True
break
if not removed:
logger.warning(f"尝试移除不存在的资源: {resource}")
return False
# 递归清除uuid映射
self._remove_uuid_mapping(resource)
# 清除 resource2parent_resource 中与该资源相关的映射
# 需要清除1) 该资源作为 key 的映射 2) 该资源作为 value 的映射
keys_to_remove = []
for key, value in self.resource2parent_resource.items():
if id(value) == resource_id:
keys_to_remove.append(key)
if resource_id in self.resource2parent_resource:
keys_to_remove.append(resource_id)
for key in keys_to_remove:
self.resource2parent_resource.pop(key, None)
logger.debug(f"成功移除资源: {resource}")
return True
def clear_resource(self):
"""清空所有资源"""
self.resources = []
self.uuid_to_resources.clear()
self.resource2parent_resource.clear()
def figure_resource(self, query_resource, try_mode=False):
if isinstance(query_resource, list):
return [self.figure_resource(r, try_mode) for r in query_resource]
elif (
isinstance(query_resource, dict) and "id" not in query_resource and "name" not in query_resource
isinstance(query_resource, dict)
and "id" not in query_resource
and "name" not in query_resource
and "uuid" not in query_resource
): # 临时处理要删除的driver有太多类型错误标注
return [self.figure_resource(r, try_mode) for r in query_resource.values()]
# 优先尝试通过 uuid 查找
res_uuid = None
if isinstance(query_resource, dict):
res_uuid = query_resource.get("uuid")
else:
res_uuid = getattr(query_resource, "unilabos_uuid", None)
# 如果有 uuid优先使用 uuid 查找
if res_uuid:
res_list = []
for r in self.resources:
if isinstance(query_resource, dict):
res_list.extend(self.loop_find_resource(r, object, "uuid", res_uuid))
else:
res_list.extend(self.loop_find_resource(r, type(query_resource), "unilabos_uuid", res_uuid))
if not try_mode:
assert len(res_list) > 0, f"没有找到资源 (uuid={res_uuid}),请检查资源是否存在"
assert len(res_list) == 1, f"通过uuid={res_uuid} 找到多个资源,请检查资源是否唯一: {res_list}"
else:
return [i[1] for i in res_list]
self.resource2parent_resource[id(query_resource)] = res_list[0][0]
self.resource2parent_resource[id(res_list[0][1])] = res_list[0][0]
return res_list[0][1]
# 回退到 id/name 查找
res_id = (
query_resource.id # type: ignore
if hasattr(query_resource, "id")
@@ -711,7 +847,7 @@ class DeviceNodeResourceTracker(object):
identifier_key = "id" if res_id else "name"
resource_cls_type = type(query_resource)
if res_identifier is None:
logger.warning(f"resource {query_resource} 没有idname暂不能对应figure")
logger.warning(f"resource {query_resource} 没有idname或uuid暂不能对应figure")
res_list = []
for r in self.resources:
if isinstance(query_resource, dict):
@@ -744,12 +880,16 @@ class DeviceNodeResourceTracker(object):
)
if issubclass(type(resource), target_resource_cls_type):
if target_resource_cls_type == dict:
# 对于字典类型,直接检查 identifier_key
if identifier_key in resource:
if resource[identifier_key] == compare_value:
res_list.append((parent_res, resource))
elif hasattr(resource, identifier_key):
if getattr(resource, identifier_key) == compare_value:
res_list.append((parent_res, resource))
else:
# 对于实例类型,需要特殊处理 uuid 字段
# 如果查找的是 unilabos_uuid使用 getattr
if hasattr(resource, identifier_key):
if getattr(resource, identifier_key) == compare_value:
res_list.append((parent_res, resource))
return res_list
def filter_find_list(self, res_list, compare_std_dict):
@@ -803,21 +943,15 @@ if __name__ == "__main__":
print(f" - 所有节点数量: {len(resource_tree_set.all_nodes)}")
# 3. 将 ResourceTreeSet 转换回 PLR 资源
plr_resources, name_to_uuid_maps = resource_tree_set.to_plr_resources()
plr_resources = resource_tree_set.to_plr_resources()
converted_plate = plr_resources[0]
print(f"\n3. 转换回 PLR 资源: {converted_plate.name}")
print(f" - 子节点数量: {len(converted_plate.children)}")
if converted_plate.children:
print(f" - 第一个子节点: {converted_plate.children[0].name}")
# 4. 验证 UUID 映射
name_to_uuid = name_to_uuid_maps[0]
print(f"\n4. UUID 映射:")
print(f" - 映射条目数: {len(name_to_uuid)}")
print(f" - 示例映射: {list(name_to_uuid.items())[:3]}")
# 5. 验证 unilabos_uuid 属性
print(f"\n5. 验证 unilabos_uuid 设置:")
# 4. 验证 unilabos_uuid 属性
print(f"\n4. 验证 unilabos_uuid 设置:")
if hasattr(converted_plate, "unilabos_uuid"):
print(f" - 根节点 UUID: {getattr(converted_plate, 'unilabos_uuid')}")
if converted_plate.children and hasattr(converted_plate.children[0], "unilabos_uuid"):
@@ -825,18 +959,18 @@ if __name__ == "__main__":
else:
print(" - 警告: unilabos_uuid 未设置")
# 6. 验证 UUID 保持不变
print(f"\n6. 验证 UUID 在往返过程中保持不变:")
# 5. 验证 UUID 保持不变
print(f"\n5. 验证 UUID 在往返过程中保持不变:")
original_uuid = getattr(original_plate, "unilabos_uuid")
converted_uuid = getattr(converted_plate, "unilabos_uuid")
print(f" - 原始 UUID: {original_uuid}")
print(f" - 转换后 UUID: {converted_uuid}")
print(f" - UUID 保持不变: {original_uuid == converted_uuid}")
# 7. 再次往返转换,验证稳定性
# 6. 再次往返转换,验证稳定性
resource_tree_set_2 = ResourceTreeSet.from_plr_resources([converted_plate])
plr_resources_2, name_to_uuid_maps_2 = resource_tree_set_2.to_plr_resources()
print(f"\n7. 第二次往返转换:")
plr_resources_2 = resource_tree_set_2.to_plr_resources()
print(f"\n6. 第二次往返转换:")
print(f" - 资源名称: {plr_resources_2[0].name}")
print(f" - 子节点数量: {len(plr_resources_2[0].children)}")
print(f" - UUID 依然保持: {getattr(plr_resources_2[0], 'unilabos_uuid') == original_uuid}")