支持选择器注册表自动生成

支持转运物料
This commit is contained in:
Xuwznln
2025-10-11 00:57:22 +08:00
parent 67c01271b7
commit 609b6006e8
31 changed files with 4268 additions and 278 deletions

View File

@@ -345,111 +345,6 @@ class BaseROS2DeviceNode(Node, Generic[T]):
res.response = ""
return res
async def s2c_resource_tree(req: SerialCommand_Request, res: SerialCommand_Response):
"""
处理资源树更新请求
支持三种操作:
- add: 添加新资源到资源树
- update: 更新现有资源
- remove: 从资源树中移除资源
"""
try:
data = json.loads(req.command)
results = []
for i in data:
action = i.get("action") # remove, add, update
resources_uuid: List[str] = i.get("data") # 资源数据
self.lab_logger().info(
f"[Resource Tree Update] Processing {action} operation, "
f"resources count: {len(resources_uuid)}"
)
tree_set = None
if action in ["add", "update"]:
response: SerialCommand.Response = await self._resource_clients[
"c2s_update_resource_tree"
].call_async(
SerialCommand.Request(
command=json.dumps(
{"data": {"data": resources_uuid, "with_children": False}, "action": "get"}
)
)
) # type: ignore
raw_nodes = json.loads(response.response)
nodes = [ResourceDictInstance.get_resource_instance_from_dict(n) for n in raw_nodes]
uuids_to_nodes = {u["uuid"]: n for u, n in zip(raw_nodes, nodes)}
for u, n in zip(raw_nodes, nodes):
n.res_content.parent = uuids_to_nodes.get(u["parent_uuid"]).res_content if u["parent_uuid"] in uuids_to_nodes else None
print(n.res_content.parent)
tree_set = ResourceTreeSet.from_nested_list(nodes)
try:
if action == "add":
# 添加资源到资源跟踪器
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(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)
results.append({"success": True, "action": "update"})
elif action == "remove":
# 移除资源
func = getattr(self.driver_instance, "resource_tree_remove", None)
if callable(func):
resources_instances: List[ResourcePLR] = [self.resource_tracker.uuid_to_resources[i] for
i in resources_uuid]
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)}"
self.lab_logger().error(f"[Resource Tree Update] {error_msg}")
self.lab_logger().error(traceback.format_exc())
results.append({"success": False, "action": action, "error": error_msg})
# 返回处理结果
result_json = {"results": results, "total": len(data)}
res.response = json.dumps(result_json, ensure_ascii=False)
self.lab_logger().info(f"[Resource Tree Update] Completed processing {len(data)} operations")
except json.JSONDecodeError as e:
error_msg = f"Invalid JSON format: {str(e)}"
self.lab_logger().error(f"[Resource Tree Update] {error_msg}")
res.response = json.dumps({"success": False, "error": error_msg}, ensure_ascii=False)
except Exception as e:
error_msg = f"Unexpected error: {str(e)}"
self.lab_logger().error(f"[Resource Tree Update] {error_msg}")
self.lab_logger().error(traceback.format_exc())
res.response = json.dumps({"success": False, "error": error_msg}, ensure_ascii=False)
return res
async def append_resource(req: SerialCommand_Request, res: SerialCommand_Response):
# 物料传输到对应的node节点
@@ -644,7 +539,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
"s2c_resource_tree": self.create_service(
SerialCommand,
f"/srv{self.namespace}/s2c_resource_tree",
s2c_resource_tree, # type: ignore
self.s2c_resource_tree, # type: ignore
callback_group=self.callback_group,
),
}
@@ -667,6 +562,159 @@ class BaseROS2DeviceNode(Node, Generic[T]):
self.lab_logger().error(traceback.format_exc())
self.lab_logger().debug(f"资源更新结果: {response}")
async def s2c_resource_tree(self, req: SerialCommand_Request, res: SerialCommand_Response):
"""
处理资源树更新请求
支持三种操作:
- add: 添加新资源到资源树
- update: 更新现有资源
- remove: 从资源树中移除资源
"""
try:
data = json.loads(req.command)
results = []
for i in data:
action = i.get("action") # remove, add, update
resources_uuid: List[str] = i.get("data") # 资源数据
self.lab_logger().info(
f"[Resource Tree Update] Processing {action} operation, "
f"resources count: {len(resources_uuid)}"
)
tree_set = None
if action in ["add", "update"]:
response: SerialCommand.Response = await self._resource_clients[
"c2s_update_resource_tree"
].call_async(
SerialCommand.Request(
command=json.dumps(
{"data": {"data": resources_uuid, "with_children": False}, "action": "get"}
)
)
) # type: ignore
raw_nodes = json.loads(response.response)
tree_set = ResourceTreeSet.from_raw_list(raw_nodes)
try:
if action == "add":
# 添加资源到资源跟踪器
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(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(plr_resources)
results.append({"success": True, "action": "update"})
elif action == "remove":
# 移除资源
plr_resources: List[ResourcePLR] = [self.resource_tracker.uuid_to_resources[i] for
i in resources_uuid]
func = getattr(self.driver_instance, "resource_tree_remove", None)
if callable(func):
func(plr_resources)
for plr_resource in plr_resources:
plr_resource.parent.unassign_child_resource(plr_resource)
self.resource_tracker.remove_resource(plr_resource)
results.append({"success": True, "action": "remove"})
except Exception as e:
error_msg = f"Error processing {action} operation: {str(e)}"
self.lab_logger().error(f"[Resource Tree Update] {error_msg}")
self.lab_logger().error(traceback.format_exc())
results.append({"success": False, "action": action, "error": error_msg})
# 返回处理结果
result_json = {"results": results, "total": len(data)}
res.response = json.dumps(result_json, ensure_ascii=False)
self.lab_logger().info(f"[Resource Tree Update] Completed processing {len(data)} operations")
except json.JSONDecodeError as e:
error_msg = f"Invalid JSON format: {str(e)}"
self.lab_logger().error(f"[Resource Tree Update] {error_msg}")
res.response = json.dumps({"success": False, "error": error_msg}, ensure_ascii=False)
except Exception as e:
error_msg = f"Unexpected error: {str(e)}"
self.lab_logger().error(f"[Resource Tree Update] {error_msg}")
self.lab_logger().error(traceback.format_exc())
res.response = json.dumps({"success": False, "error": error_msg}, ensure_ascii=False)
return res
async def transfer_resource_to_another(self, plr_resources: List["ResourcePLR"], target_device_id, target_resource_uuid: str):
# 准备工作
uids = []
for plr_resource in plr_resources:
uid = getattr(plr_resource, "unilabos_uuid", None)
if uid is None:
raise ValueError(f"物料{plr_resource}没有unilabos_uuid属性无法转运")
uids.append(uid)
srv_address = f"/srv{target_device_id}/s2c_resource_tree"
sclient = self.create_client(SerialCommand, srv_address)
# 等待服务可用(设置超时)
if not sclient.wait_for_service(timeout_sec=5.0):
self.lab_logger().error(f"[{self.device_id} Node-Resource] Service {srv_address} not available")
raise ValueError(f"[{self.device_id} Node-Resource] Service {srv_address} not available")
# 先从当前节点移除资源
await self.s2c_resource_tree(SerialCommand_Request(command=json.dumps([{
"action": "remove",
"data": uids # 只移除父节点
}], ensure_ascii=False)), SerialCommand_Response())
# 通知云端转运资源
tree_set = ResourceTreeSet.from_plr_resources(plr_resources)
for root_node in tree_set.root_nodes:
root_node.res_content.parent = None
root_node.res_content.parent_uuid = target_resource_uuid
r = SerialCommand.Request()
r.command = json.dumps({"data": {"data": tree_set.dump()}, "action": "update"}) # 和Update Resource一致
response: SerialCommand_Response = await self._resource_clients["c2s_update_resource_tree"].call_async(r) # type: ignore
self.lab_logger().info(f"资源云端转运到{target_device_id}结果: {response.response}")
# 创建请求
request = SerialCommand.Request()
request.command = json.dumps([{
"action": "add",
"data": tree_set.all_nodes_uuid # 只添加父节点,子节点会自动添加
}], ensure_ascii=False)
future = sclient.call_async(request)
timeout = 30.0
start_time = time.time()
while not future.done():
if time.time() - start_time > timeout:
self.lab_logger().error(f"[{self.device_id} Node-Resource] Timeout waiting for response from {target_device_id}")
return False
time.sleep(0.05)
self.lab_logger().info(f"资源本地增加到{target_device_id}结果: {response.response}")
return None
def register_device(self):
"""向注册表中注册设备信息"""
topics_info = self._property_publishers.copy()

View File

@@ -247,7 +247,6 @@ class HostNode(BaseROS2DeviceNode):
else:
resource_instance = self.resource_tracker.figure_resource({"name": node.res_content.name})
self._resource_tracker.loop_update_uuid(resource_instance, uuid_mapping)
except Exception as ex:
self.lab_logger().error("[Host Node-Resource] 添加物料出错!")
self.lab_logger().error(traceback.format_exc())
@@ -1323,7 +1322,7 @@ class HostNode(BaseROS2DeviceNode):
if time.time() - start_time > timeout:
self.lab_logger().error(f"[Host Node-Resource] Timeout waiting for response from {device_id}")
return False
time.sleep(0.01)
time.sleep(0.05)
response = future.result()
self.lab_logger().info(

View File

@@ -454,6 +454,59 @@ class ResourceTreeSet(object):
return plr_resources
@classmethod
def from_raw_list(cls, raw_list: List[Dict[str, Any]]) -> "ResourceTreeSet":
"""
从原始字典列表创建 ResourceTreeSet自动建立 parent-children 关系
Args:
raw_list: 原始字典列表,每个字典代表一个资源节点
Returns:
ResourceTreeSet 实例
Raises:
ValueError: 当建立关系时发现不一致
"""
# 第一步:将字典列表转换为 ResourceDictInstance 列表
instances = [ResourceDictInstance.get_resource_instance_from_dict(node_dict) for node_dict in raw_list]
# 第二步:建立映射关系
uuid_to_instance: Dict[str, ResourceDictInstance] = {}
id_to_instance: Dict[str, ResourceDictInstance] = {}
for raw_node, instance in zip(raw_list, instances):
# 建立 uuid 映射
if instance.res_content.uuid:
uuid_to_instance[instance.res_content.uuid] = instance
# 建立 id 映射
if instance.res_content.id:
id_to_instance[instance.res_content.id] = instance
# 第三步:建立 parent-children 关系
for raw_node, instance in zip(raw_list, instances):
# 优先使用 parent_uuid 进行匹配,如果不存在则使用 parent (id)
parent_uuid = raw_node.get("parent_uuid")
parent_id = raw_node.get("parent")
parent_instance = None
# 优先用 parent_uuid 匹配
if parent_uuid and parent_uuid in uuid_to_instance:
parent_instance = uuid_to_instance[parent_uuid]
# 否则用 parent (id) 匹配
elif parent_id and parent_id in id_to_instance:
parent_instance = id_to_instance[parent_id]
# 设置 parent 引用并建立 children 关系
if parent_instance:
instance.res_content.parent = parent_instance.res_content
# 将当前节点添加到父节点的 children 列表(避免重复添加)
if instance not in parent_instance.children:
parent_instance.children.append(instance)
# 第四步:使用 from_nested_list 创建 ResourceTreeSet
return cls.from_nested_list(instances)
@classmethod
def from_nested_list(cls, nested_list: List[ResourceDictInstance]) -> "ResourceTreeSet":
"""
@@ -497,6 +550,16 @@ class ResourceTreeSet(object):
"""
return [node for tree in self.trees for node in tree.get_all_nodes()]
@property
def all_nodes_uuid(self) -> List[str]:
"""
获取所有树中的所有节点
Returns:
所有节点的资源实例列表
"""
return [node.res_content.uuid for tree in self.trees for node in tree.get_all_nodes()]
def find_by_uuid(self, target_uuid: str) -> Optional[ResourceDictInstance]:
"""
在所有树中通过uuid查找节点
@@ -513,6 +576,116 @@ class ResourceTreeSet(object):
return result
return None
def merge_remote_resources(self, remote_tree_set: "ResourceTreeSet") -> "ResourceTreeSet":
"""
将远端物料同步到本地物料中(以子树为单位)
同步规则:
1. 一级节点(根节点):如果不存在的物料,引入整个子树
2. 一级设备下的二级物料:如果不存在,引入整个子树
3. 二级设备下的三级物料:如果不存在,引入整个子树
如果存在则跳过并提示
Args:
remote_tree_set: 远端的资源树集合
Returns:
合并后的资源树集合self
"""
# 构建本地映射:一级 device id -> 根节点实例
local_device_map: Dict[str, ResourceDictInstance] = {}
for root_node in self.root_nodes:
if root_node.res_content.type == "device":
local_device_map[root_node.res_content.id] = root_node
# 记录需要添加的新根节点(不属于任何 device 的物料)
new_root_nodes: List[ResourceDictInstance] = []
# 遍历远端根节点
for remote_root in remote_tree_set.root_nodes:
remote_root_id = remote_root.res_content.id
remote_root_type = remote_root.res_content.type
if remote_root_type == "device":
# 情况1: 一级是 device
if remote_root_id not in local_device_map:
logger.warning(f"Device '{remote_root_id}' 在本地不存在,跳过该 device 下的物料同步")
continue
local_device = local_device_map[remote_root_id]
# 构建本地一级 device 下的子节点映射
local_children_map = {child.res_content.name: child for child in local_device.children}
# 遍历远端一级 device 的子节点
for remote_child in remote_root.children:
remote_child_name = remote_child.res_content.name
remote_child_type = remote_child.res_content.type
if remote_child_type == "device":
# 情况2: 二级是 device
if remote_child_name not in local_children_map:
logger.warning(f"Device '{remote_root_id}/{remote_child_name}' 在本地不存在,跳过")
continue
local_sub_device = local_children_map[remote_child_name]
# 构建本地二级 device 下的子节点映射
local_sub_children_map = {child.res_content.name: child for child in local_sub_device.children}
# 遍历远端二级 device 的子节点(三级物料)
added_count = 0
for remote_material in remote_child.children:
remote_material_name = remote_material.res_content.name
# 情况3: 三级物料
if remote_material_name not in local_sub_children_map:
# 引入整个子树
remote_material.res_content.parent = local_sub_device.res_content
local_sub_device.children.append(remote_material)
added_count += 1
else:
logger.info(
f"物料 '{remote_root_id}/{remote_child_name}/{remote_material_name}' "
f"已存在,跳过"
)
if added_count > 0:
logger.info(
f"Device '{remote_root_id}/{remote_child_name}': "
f"从远端同步了 {added_count} 个物料子树"
)
else:
# 情况2: 二级是物料(不是 device
if remote_child_name not in local_children_map:
# 引入整个子树
remote_child.res_content.parent = local_device.res_content
local_device.children.append(remote_child)
logger.info(f"Device '{remote_root_id}': 从远端同步物料子树 '{remote_child_name}'")
else:
logger.info(f"物料 '{remote_root_id}/{remote_child_name}' 已存在,跳过")
else:
# 情况1: 一级节点是物料(不是 device
# 检查是否已存在
existing = False
for local_root in self.root_nodes:
if local_root.res_content.name == remote_root.res_content.name:
existing = True
logger.info(f"根节点物料 '{remote_root.res_content.name}' 已存在,跳过")
break
if not existing:
# 引入整个子树
new_root_nodes.append(remote_root)
logger.info(f"添加远端独立物料根节点子树: '{remote_root_id}'")
# 将新的根节点添加到本地树集合
if new_root_nodes:
for new_root in new_root_nodes:
self.trees.append(ResourceTreeInstance(new_root))
return self
def dump(self) -> List[List[Dict[str, Any]]]:
"""
将 ResourceTreeSet 序列化为嵌套列表格式