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 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") print_status(f"已创建 local_config.py 路径: {config_path}", "info")
os._exit(1)
else: else:
os._exit(1) os._exit(1)
# 加载配置文件 # 加载配置文件

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import sys
import inspect import inspect
import importlib import importlib
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List from typing import Any, Dict, List, Union, Tuple
import yaml import yaml
@@ -294,7 +294,7 @@ class Registry:
logger.warning(f"[UniLab Registry] 设备 {device_id}{field_name} 类型为空,跳过替换") logger.warning(f"[UniLab Registry] 设备 {device_id}{field_name} 类型为空,跳过替换")
return type_name return type_name
convert_manager = { # 将python基本对象转为ros2基本对象 convert_manager = { # 将python基本对象转为ros2基本对象
"str": "String", "builtins:str": "String",
"bool": "Bool", "bool": "Bool",
"int": "Int64", "int": "Int64",
"float": "Float64", "float": "Float64",
@@ -310,34 +310,70 @@ class Registry:
logger.error(f"[UniLab Registry] 无法找到类型 '{type_name}' 用于设备 {device_id}{field_name}") logger.error(f"[UniLab Registry] 无法找到类型 '{type_name}' 用于设备 {device_id}{field_name}")
sys.exit(1) 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( def _generate_schema_from_info(
self, self,
param_name: str, param_name: str,
param_type: str, param_type: Union[str, Tuple[str]],
param_default: Any, param_default: Any,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" """
根据参数信息生成JSON Schema 根据参数信息生成JSON Schema
""" """
prop_schema = {} prop_schema = {}
# 根据类型设置schema FIXME 不完整
if param_type: # 处理嵌套类型Tuple[str]
param_type_lower = param_type.lower() if isinstance(param_type, tuple):
if param_type_lower in ["str", "string"]: if len(param_type) == 2:
prop_schema["type"] = "string" outer_type, inner_type = param_type
elif param_type_lower in ["int", "integer"]: outer_json_type = self._get_json_schema_type(outer_type)
prop_schema["type"] = "integer" inner_json_type = self._get_json_schema_type(inner_type)
elif param_type_lower in ["float", "number"]:
prop_schema["type"] = "number" prop_schema["type"] = outer_json_type
elif param_type_lower in ["bool", "boolean"]:
prop_schema["type"] = "boolean" # 根据外层类型设置内层类型信息
elif param_type_lower in ["list", "array"]: if outer_json_type == "array":
prop_schema["type"] = "array" prop_schema["items"] = {"type": inner_json_type}
elif param_type_lower in ["dict", "object"]: elif outer_json_type == "object":
prop_schema["type"] = "object" prop_schema["additionalProperties"] = {"type": inner_json_type}
else: else:
# 默认为字符串类型 # 不是标准的嵌套类型,默认为字符串
prop_schema["type"] = "string" prop_schema["type"] = "string"
else:
# 处理非嵌套类型
if param_type:
prop_schema["type"] = self._get_json_schema_type(param_type)
else: else:
# 如果没有类型信息,默认为字符串 # 如果没有类型信息,默认为字符串
prop_schema["type"] = "string" prop_schema["type"] = "string"
@@ -456,7 +492,7 @@ class Registry:
{k: v["return_type"] for k, v in enhanced_info["status_methods"].items()} {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(): 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便于显示 status_type = "String" # 替换成ROS的String便于显示
device_config["class"]["status_types"][status_name] = status_type device_config["class"]["status_types"][status_name] = status_type
target_type = self._replace_type_with_class(status_type, device_id, f"状态 {status_name}") 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: else:
final_resource = [convert_resources_to_type([i], final_type)[0] for i in resources_list] final_resource = [convert_resources_to_type([i], final_type)[0] for i in resources_list]
try: 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: except Exception as e:
self.lab_logger().error(f"物料实例获取失败: {e}\n{traceback.format_exc()}") self.lab_logger().error(f"物料实例获取失败: {e}\n{traceback.format_exc()}")
error_skip = True error_skip = True

View File

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

View File

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