update registry with nested obj

This commit is contained in:
Xuwznln
2025-09-19 03:44:18 +08:00
parent e5006285df
commit 01f8816597
14 changed files with 144 additions and 81 deletions

View File

@@ -208,7 +208,6 @@ def main():
os.path.join(os.path.dirname(os.path.dirname(__file__)), "config", "example_config.py"), config_path
)
print_status(f"已创建 local_config.py 路径: {config_path}", "info")
os._exit(1)
else:
os._exit(1)
# 加载配置文件

View File

@@ -384,7 +384,7 @@ class MessageProcessor:
"""停止消息处理线程"""
self.is_running = False
if self.thread and self.thread.is_alive():
self.thread.join(timeout=5)
self.thread.join(timeout=2)
logger.info("[MessageProcessor] Stopped")
def _run(self):
@@ -832,7 +832,7 @@ class QueueProcessor:
"""停止队列处理线程"""
self.is_running = False
if self.thread and self.thread.is_alive():
self.thread.join(timeout=5)
self.thread.join(timeout=2)
logger.info("[QueueProcessor] Stopped")
def _run(self):

View File

@@ -94,7 +94,7 @@ serial:
port:
type: string
resource_tracker:
type: string
type: object
required:
- device_id
- port

View File

@@ -63,7 +63,7 @@ camera.USB:
default: 0.1
type: number
resource_tracker:
type: string
type: object
required: []
type: object
data:

View File

@@ -170,7 +170,7 @@ hplc.agilent:
module: unilabos.devices.hplc.AgilentHPLC:HPLCDriver
status_types:
could_run: bool
data_file: list
data_file: String
device_status: str
driver_init_ok: bool
finish_status: str
@@ -195,6 +195,8 @@ hplc.agilent:
could_run:
type: boolean
data_file:
items:
type: string
type: array
device_status:
type: string

View File

@@ -4508,14 +4508,22 @@ liquid_handler.biomek:
bind_parent_id:
type: string
liquid_input_slot:
items:
type: integer
type: array
liquid_type:
items:
type: string
type: array
liquid_volume:
items:
type: integer
type: array
resource_tracker:
type: string
type: object
resources:
items:
type: object
type: array
slot_on_deck:
type: integer
@@ -4559,10 +4567,16 @@ liquid_handler.biomek:
id:
type: string
liquid_input_wells:
items:
type: string
type: array
liquid_type:
items:
type: string
type: array
liquid_volume:
items:
type: integer
type: array
parent:
type: string
@@ -6039,6 +6053,8 @@ liquid_handler.prcxi:
properties:
none_keys:
default: []
items:
type: string
type: array
protocol_author:
default: ''
@@ -6139,7 +6155,7 @@ liquid_handler.prcxi:
default: 0
type: number
well:
type: string
type: object
required:
- well
type: object
@@ -8358,7 +8374,7 @@ liquid_handler.prcxi:
default: false
type: string
deck:
type: string
type: object
host:
type: string
matrix_id:

View File

@@ -832,7 +832,7 @@ syringe_pump_with_valve.runze.SY03B-T06:
default: 25.0
type: number
mode:
type: string
type: object
port:
type: string
required:
@@ -1352,7 +1352,7 @@ syringe_pump_with_valve.runze.SY03B-T08:
default: 25.0
type: number
mode:
type: string
type: object
port:
type: string
required:

View File

@@ -133,7 +133,7 @@ robotic_arm.SCARA_with_slider.virtual:
goal:
properties:
ros_node:
type: string
type: object
required:
- ros_node
type: object
@@ -753,7 +753,7 @@ robotic_arm.elite:
module: unilabos.devices.arm.elite_robot:EliteRobot
status_types:
actual_joint_positions: String
arm_pose: list
arm_pose: String
type: python
config_info: []
description: Elite robot arm
@@ -775,6 +775,8 @@ robotic_arm.elite:
actual_joint_positions:
type: string
arm_pose:
items:
type: number
type: array
required:
- arm_pose

View File

@@ -37,7 +37,7 @@ linear_motion.grbl:
goal:
properties:
position:
type: string
type: object
required:
- position
type: object
@@ -450,6 +450,8 @@ linear_motion.grbl:
- 0
- -80
- 0
items:
type: integer
type: array
port:
type: string
@@ -459,7 +461,7 @@ linear_motion.grbl:
data:
properties:
position:
type: string
type: object
spindle_speed:
type: number
status:
@@ -605,7 +607,7 @@ linear_motion.toyo_xyz.sim:
goal:
properties:
ros_node:
type: string
type: object
required:
- ros_node
type: object

View File

@@ -6129,7 +6129,7 @@ workstation:
protocol_type:
type: string
resource_tracker:
type: string
type: object
required:
- device_id
- children
@@ -6171,14 +6171,22 @@ workstation.example:
bind_parent_id:
type: string
liquid_input_slot:
items:
type: integer
type: array
liquid_type:
items:
type: string
type: array
liquid_volume:
items:
type: integer
type: array
resource_tracker:
type: string
type: object
resources:
items:
type: object
type: array
slot_on_deck:
type: integer
@@ -6213,9 +6221,9 @@ workstation.example:
goal:
properties:
base_plate:
type: string
type: object
tip_rack:
type: string
type: object
required:
- tip_rack
- base_plate
@@ -6241,9 +6249,9 @@ workstation.example:
goal:
properties:
from_plate:
type: string
type: object
to_base_plate:
type: string
type: object
required:
- from_plate
- to_base_plate
@@ -6271,7 +6279,7 @@ workstation.example:
protocol_type:
type: string
resource_tracker:
type: string
type: object
required:
- device_id
- children

View File

@@ -5,7 +5,7 @@ import sys
import inspect
import importlib
from pathlib import Path
from typing import Any, Dict, List
from typing import Any, Dict, List, Union, Tuple
import yaml
@@ -294,7 +294,7 @@ class Registry:
logger.warning(f"[UniLab Registry] 设备 {device_id}{field_name} 类型为空,跳过替换")
return type_name
convert_manager = { # 将python基本对象转为ros2基本对象
"str": "String",
"builtins:str": "String",
"bool": "Bool",
"int": "Int64",
"float": "Float64",
@@ -310,34 +310,70 @@ class Registry:
logger.error(f"[UniLab Registry] 无法找到类型 '{type_name}' 用于设备 {device_id}{field_name}")
sys.exit(1)
def _get_json_schema_type(self, type_str: str) -> str:
"""
根据类型字符串返回对应的JSON Schema类型
Args:
type_str: 类型字符串
Returns:
JSON Schema类型字符串
"""
type_lower = type_str.lower()
type_mapping = {
("str", "string"): "string",
("int", "integer"): "integer",
("float", "number"): "number",
("bool", "boolean"): "boolean",
("list", "array"): "array",
("dict", "object"): "object",
}
# 遍历映射找到匹配的类型
for type_variants, json_type in type_mapping.items():
if type_lower in type_variants:
return json_type
# 特殊处理包含冒号的类型如ROS消息类型
if ":" in type_lower:
return "object"
# 默认返回字符串类型
return "string"
def _generate_schema_from_info(
self,
param_name: str,
param_type: str,
param_type: Union[str, Tuple[str]],
param_default: Any,
) -> Dict[str, Any]:
"""
根据参数信息生成JSON Schema
"""
prop_schema = {}
# 根据类型设置schema FIXME 不完整
if param_type:
param_type_lower = param_type.lower()
if param_type_lower in ["str", "string"]:
prop_schema["type"] = "string"
elif param_type_lower in ["int", "integer"]:
prop_schema["type"] = "integer"
elif param_type_lower in ["float", "number"]:
prop_schema["type"] = "number"
elif param_type_lower in ["bool", "boolean"]:
prop_schema["type"] = "boolean"
elif param_type_lower in ["list", "array"]:
prop_schema["type"] = "array"
elif param_type_lower in ["dict", "object"]:
prop_schema["type"] = "object"
# 处理嵌套类型Tuple[str]
if isinstance(param_type, tuple):
if len(param_type) == 2:
outer_type, inner_type = param_type
outer_json_type = self._get_json_schema_type(outer_type)
inner_json_type = self._get_json_schema_type(inner_type)
prop_schema["type"] = outer_json_type
# 根据外层类型设置内层类型信息
if outer_json_type == "array":
prop_schema["items"] = {"type": inner_json_type}
elif outer_json_type == "object":
prop_schema["additionalProperties"] = {"type": inner_json_type}
else:
# 默认为字符串类型
# 不是标准的嵌套类型,默认为字符串
prop_schema["type"] = "string"
else:
# 处理非嵌套类型
if param_type:
prop_schema["type"] = self._get_json_schema_type(param_type)
else:
# 如果没有类型信息,默认为字符串
prop_schema["type"] = "string"
@@ -456,7 +492,7 @@ class Registry:
{k: v["return_type"] for k, v in enhanced_info["status_methods"].items()}
)
for status_name, status_type in device_config["class"]["status_types"].items():
if status_type in ["Any", "None", "Unknown"]:
if isinstance(status_type, tuple) or status_type in ["Any", "None", "Unknown"]:
status_type = "String" # 替换成ROS的String便于显示
device_config["class"]["status_types"][status_name] = status_type
target_type = self._replace_type_with_class(status_type, device_id, f"状态 {status_name}")

View File

@@ -697,7 +697,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
else:
final_resource = [convert_resources_to_type([i], final_type)[0] for i in resources_list]
try:
action_kwargs[k] = self.resource_tracker.figure_resource(final_resource, try_mode=True)
action_kwargs[k] = self.resource_tracker.figure_resource(final_resource, try_mode=False)
except Exception as e:
self.lab_logger().error(f"物料实例获取失败: {e}\n{traceback.format_exc()}")
error_skip = True

View File

@@ -199,22 +199,18 @@ class HostNode(BaseROS2DeviceNode):
"children": [],
},
)
resource_with_parent_name = []
resource_with_dirs_name = []
resource_ids_to_instance = {i["id"]: i for i in resources_config}
resource_name_to_with_parent_name = {}
for res in resources_config:
# if res.get("parent") and res.get("type") == "device" and res.get("class"):
# parent_id = res.get("parent")
# parent_res = resource_ids_to_instance[parent_id]
# if parent_res.get("type") == "device" and parent_res.get("class"):
# resource_with_parent_name.append(copy.deepcopy(res))
# resource_name_to_with_parent_name[resource_with_parent_name[-1]["id"]] = f"{parent_res['id']}/{res['id']}"
# resource_with_parent_name[-1]["id"] = f"{parent_res['id']}/{res['id']}"
# continue
resource_with_parent_name.append(copy.deepcopy(res))
# for edge in self.resources_edge_config:
# edge["source"] = resource_name_to_with_parent_name.get(edge.get("source"), edge.get("source"))
# edge["target"] = resource_name_to_with_parent_name.get(edge.get("target"), edge.get("target"))
temp_res = res
res_paths = [res]
while temp_res.get("parent"):
temp_res = resource_ids_to_instance[temp_res.get("parent")]
res_paths.append(temp_res)
dirs = "/" + "/".join([res["id"] for res in res_paths[::-1]])
new_res = copy.deepcopy(res)
new_res["data"]["unilabos_dirs"] = dirs
resource_with_dirs_name.append(new_res)
try:
for bridge in self.bridges:
if hasattr(bridge, "resource_add"):
@@ -222,7 +218,12 @@ class HostNode(BaseROS2DeviceNode):
client: HTTPClient = bridge
resource_start_time = time.time()
resource_add_res = client.resource_add(add_schema(resource_with_parent_name), False)
resource_add_res = client.resource_add(add_schema(resources_config), False)
# DEBUG ONLY
# for i in resource_with_dirs_name:
# http_req = self.bridges[-1].resource_get(i["data"]["unilabos_dirs"], True)
# res = self._resource_get_process(http_req)
# print(res)
resource_end_time = time.time()
self.lab_logger().info(
f"[Host Node-Resource] 物料上传 {round(resource_end_time - resource_start_time, 5) * 1000} ms"
@@ -871,6 +872,12 @@ class HostNode(BaseROS2DeviceNode):
self.lab_logger().info(f"[Host Node-Resource] Add request completed, success: {success}")
return response
def _resource_get_process(self, data: Dict[str, Any]):
r = data["data"]
self.lab_logger().debug(f"[Host Node-Resource] Retrieved from bridge: {len(r)} resources")
resources = [convert_to_ros_msg(Resource, resource) for resource in r]
return resources
def _resource_get_callback(self, request: ResourceGet.Request, response: ResourceGet.Response):
"""
获取资源回调
@@ -884,22 +891,14 @@ class HostNode(BaseROS2DeviceNode):
Returns:
响应对象,包含查询到的资源
"""
self.lab_logger().info(f"[Host Node-Resource] Get request for ID: {request.id}")
if len(self.bridges) > 0:
# 云上物料服务,根据 id 查询物料
try:
r = self.bridges[-1].resource_get(request.id, request.with_children)["data"]
self.lab_logger().debug(f"[Host Node-Resource] Retrieved from bridge: {len(r)} resources")
http_req = self.bridges[-1].resource_get(request.id, request.with_children)
response.resources = self._resource_get_process(http_req)
return response
except Exception as e:
self.lab_logger().error(f"[Host Node-Resource] Error retrieving from bridge: {str(e)}")
r = [resource for resource in self.resources_config if resource.get("id") == request.id]
self.lab_logger().warning(f"[Host Node-Resource] Retrieved from local: {len(r)} resources")
else:
# 本地物料服务,根据 id 查询物料
r = [resource for resource in self.resources_config if resource.get("id") == request.id]
self.lab_logger().debug(f"[Host Node-Resource] Retrieved from local: {len(r)} resources")
response.resources = [convert_to_ros_msg(Resource, resource) for resource in r]
return response

View File

@@ -12,8 +12,7 @@ import traceback
import ast
import os
from pathlib import Path
from typing import Dict, List, Any, Optional, Callable, Type
from typing import Dict, List, Any, Optional, Callable, Type, Union, Tuple
__all__ = [
"ImportManager",
@@ -383,7 +382,7 @@ class ImportManager:
signature = inspect.signature(method)
return self._get_type_string(signature.return_annotation)
def _get_type_string(self, annotation) -> str:
def _get_type_string(self, annotation) -> Union[str, Tuple[str, Any]]:
"""将类型注解转换为Class Library中可搜索的类名"""
if annotation == inspect.Parameter.empty:
return "Any" # 如果没有注解返回Any
@@ -400,7 +399,7 @@ class ImportManager:
return "Int64MultiArray"
elif isinstance(arg0, float):
return "Float64MultiArray"
return "list"
return "list", self._get_type_string(arg0)
elif origin is dict:
return "dict"
elif origin is Optional: