Merge branch 'dev' into workstation_dev_new

# Conflicts:
#	unilabos/registry/devices/work_station.yaml
This commit is contained in:
Xuwznln
2025-09-09 14:35:52 +08:00
16 changed files with 5840 additions and 1619 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -78,21 +78,23 @@ def setup_web_pages(router: APIRouter) -> None:
HTMLResponse: 渲染后的HTML页面
"""
try:
# 准备设备数据
# 准备初始数据结构这些数据将通过WebSocket实时更新
devices = []
resources = []
modules = {"names": [], "classes": [], "displayed_count": 0, "total_count": 0}
# 获取在线设备信息
# 获取在线设备信息(用于初始渲染)
ros_node_info = get_ros_node_info()
# 获取主机节点信息
# 获取主机节点信息(用于初始渲染)
host_node_info = get_host_node_info()
# 获取Registry路径信息
# 获取Registry路径信息(静态信息,不需要实时更新)
registry_info = get_registry_info()
# 获取已加载的设备
# 获取初始数据用于页面渲染后续将被WebSocket数据覆盖
if lab_registry:
devices = json.loads(json.dumps(lab_registry.obtain_registry_device_info(), ensure_ascii=False, cls=TypeEncoder))
devices = json.loads(
json.dumps(lab_registry.obtain_registry_device_info(), ensure_ascii=False, cls=TypeEncoder)
)
# 资源类型
for resource_id, resource_info in lab_registry.resource_type_registry.items():
resources.append(
@@ -103,7 +105,7 @@ def setup_web_pages(router: APIRouter) -> None:
}
)
# 获取导入的模块
# 获取导入的模块(初始数据)
if msg_converter_manager:
modules["names"] = msg_converter_manager.list_modules()
all_classes = [i for i in msg_converter_manager.list_classes() if "." in i]
@@ -171,3 +173,20 @@ def setup_web_pages(router: APIRouter) -> None:
except Exception as e:
error(f"打开文件夹时出错: {str(e)}")
return {"status": "error", "message": f"Failed to open folder: {str(e)}"}
@router.get("/registry-editor", response_class=HTMLResponse, summary="Registry Editor")
async def registry_editor_page() -> str:
"""
注册表编辑页面用于导入Python文件并生成注册表
Returns:
HTMLResponse: 渲染后的HTML页面
"""
try:
# 使用模板渲染页面
template = env.get_template("registry_editor.html")
html = template.render()
return html
except Exception as e:
error(f"生成注册表编辑页面时出错: {str(e)}")
raise HTTPException(status_code=500, detail=f"Error generating registry editor page: {str(e)}")

View File

@@ -162,7 +162,6 @@
<body>
<h1>{% block header %}UniLab{% endblock %}</h1>
{% block nav %}
<a href="/unilabos/webtic" class="home-link">Home</a>
{% endblock %}
{% block top_info %}{% endblock %}

View File

@@ -1,22 +1,25 @@
{% extends "base.html" %}
{% block title %}UniLab API{% endblock %}
{% block header %}UniLab API{% endblock %}
{% block nav %}
<a href="/status" class="status-link">System Status</a>
{% endblock %}
{% block content %}
<div class="card">
<h2>Available Endpoints</h2>
{% for route in routes %}
<div class="endpoint">
<span class="method">{{ route.method }}</span>
<a href="{{ route.path }}">{{ route.path }}</a>
<p>{{ route.summary }}</p>
</div>
{% endfor %}
{% extends "base.html" %} {% block title %}UniLab API{% endblock %} {% block
header %}UniLab API{% endblock %} {% block nav %}
<div class="nav-tabs">
<a
href="/"
class="nav-tab"
style="background-color: #2196f3; color: white"
target="_blank"
>主页</a
>
<a href="/status" class="nav-tab">状态</a>
<a href="/registry-editor" class="nav-tab" target="_blank">注册表编辑</a>
</div>
{% endblock %}
{% endblock %} {% block content %}
<div class="card">
<h2>Available Endpoints</h2>
{% for route in routes %}
<div class="endpoint">
<span class="method">{{ route.method }}</span>
<a href="{{ route.path }}">{{ route.path }}</a>
<p>{{ route.summary }}</p>
</div>
{% endfor %}
</div>
{% endblock %}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -336,7 +336,7 @@ separator.homemade:
- y
- z
- w
title: Quaternion
title: orientation
type: object
position:
properties:
@@ -350,12 +350,12 @@ separator.homemade:
- x
- y
- z
title: Point
title: position
type: object
required:
- position
- orientation
title: Pose
title: pose
type: object
sample_id:
type: string
@@ -372,7 +372,7 @@ separator.homemade:
- pose
- config
- data
title: Resource
title: vessel
type: object
required:
- vessel

View File

@@ -533,7 +533,7 @@ gripper.mock:
required:
- position
- max_effort
title: GripperCommand
title: command
type: object
required:
- command

View File

@@ -139,12 +139,12 @@ linear_motion.grbl:
required:
- sec
- nanosec
title: Time
title: stamp
type: object
required:
- stamp
- frame_id
title: Header
title: header
type: object
pose:
properties:
@@ -163,7 +163,7 @@ linear_motion.grbl:
- y
- z
- w
title: Quaternion
title: orientation
type: object
position:
properties:
@@ -177,17 +177,17 @@ linear_motion.grbl:
- x
- y
- z
title: Point
title: position
type: object
required:
- position
- orientation
title: Pose
title: pose
type: object
required:
- header
- pose
title: PoseStamped
title: current_pose
type: object
distance_remaining:
type: number
@@ -204,7 +204,7 @@ linear_motion.grbl:
required:
- sec
- nanosec
title: Duration
title: estimated_time_remaining
type: object
navigation_time:
properties:
@@ -219,7 +219,7 @@ linear_motion.grbl:
required:
- sec
- nanosec
title: Duration
title: navigation_time
type: object
number_of_poses_remaining:
maximum: 32767
@@ -262,12 +262,12 @@ linear_motion.grbl:
required:
- sec
- nanosec
title: Time
title: stamp
type: object
required:
- stamp
- frame_id
title: Header
title: header
type: object
pose:
properties:
@@ -286,7 +286,7 @@ linear_motion.grbl:
- y
- z
- w
title: Quaternion
title: orientation
type: object
position:
properties:
@@ -300,17 +300,17 @@ linear_motion.grbl:
- x
- y
- z
title: Point
title: position
type: object
required:
- position
- orientation
title: Pose
title: pose
type: object
required:
- header
- pose
title: PoseStamped
title: poses
type: object
type: array
required:
@@ -323,7 +323,7 @@ linear_motion.grbl:
result:
properties: {}
required: []
title: Empty
title: result
type: object
required:
- result
@@ -371,12 +371,12 @@ linear_motion.grbl:
required:
- sec
- nanosec
title: Time
title: stamp
type: object
required:
- stamp
- frame_id
title: Header
title: header
type: object
position:
type: number
@@ -406,7 +406,7 @@ linear_motion.grbl:
required:
- sec
- nanosec
title: Duration
title: min_duration
type: object
position:
type: number

View File

@@ -362,7 +362,7 @@ heaterstirrer.dalong:
- y
- z
- w
title: Quaternion
title: orientation
type: object
position:
properties:
@@ -376,12 +376,12 @@ heaterstirrer.dalong:
- x
- y
- z
title: Point
title: position
type: object
required:
- position
- orientation
title: Pose
title: pose
type: object
sample_id:
type: string
@@ -398,7 +398,7 @@ heaterstirrer.dalong:
- pose
- config
- data
title: Resource
title: vessel
type: object
required:
- vessel

View File

@@ -145,7 +145,7 @@ virtual_centrifuge:
- y
- z
- w
title: Quaternion
title: orientation
type: object
position:
properties:
@@ -159,12 +159,12 @@ virtual_centrifuge:
- x
- y
- z
title: Point
title: position
type: object
required:
- position
- orientation
title: Pose
title: pose
type: object
sample_id:
type: string
@@ -181,7 +181,7 @@ virtual_centrifuge:
- pose
- config
- data
title: Resource
title: vessel
type: object
required:
- vessel
@@ -446,7 +446,7 @@ virtual_column:
- y
- z
- w
title: Quaternion
title: orientation
type: object
position:
properties:
@@ -460,12 +460,12 @@ virtual_column:
- x
- y
- z
title: Point
title: position
type: object
required:
- position
- orientation
title: Pose
title: pose
type: object
sample_id:
type: string
@@ -482,7 +482,7 @@ virtual_column:
- pose
- config
- data
title: Resource
title: from_vessel
type: object
pct1:
type: string
@@ -531,7 +531,7 @@ virtual_column:
- y
- z
- w
title: Quaternion
title: orientation
type: object
position:
properties:
@@ -545,12 +545,12 @@ virtual_column:
- x
- y
- z
title: Point
title: position
type: object
required:
- position
- orientation
title: Pose
title: pose
type: object
sample_id:
type: string
@@ -567,7 +567,7 @@ virtual_column:
- pose
- config
- data
title: Resource
title: to_vessel
type: object
required:
- from_vessel
@@ -850,7 +850,7 @@ virtual_filter:
- y
- z
- w
title: Quaternion
title: orientation
type: object
position:
properties:
@@ -864,12 +864,12 @@ virtual_filter:
- x
- y
- z
title: Point
title: position
type: object
required:
- position
- orientation
title: Pose
title: pose
type: object
sample_id:
type: string
@@ -886,7 +886,7 @@ virtual_filter:
- pose
- config
- data
title: Resource
title: filtrate_vessel
type: object
stir:
type: boolean
@@ -929,7 +929,7 @@ virtual_filter:
- y
- z
- w
title: Quaternion
title: orientation
type: object
position:
properties:
@@ -943,12 +943,12 @@ virtual_filter:
- x
- y
- z
title: Point
title: position
type: object
required:
- position
- orientation
title: Pose
title: pose
type: object
sample_id:
type: string
@@ -965,7 +965,7 @@ virtual_filter:
- pose
- config
- data
title: Resource
title: vessel
type: object
volume:
type: number
@@ -1071,8 +1071,8 @@ virtual_filter:
- status
- progress
- current_temp
- filtered_volume
- current_status
- filtered_volume
- message
- max_temp
- max_stir_speed
@@ -1455,7 +1455,7 @@ virtual_heatchill:
- y
- z
- w
title: Quaternion
title: orientation
type: object
position:
properties:
@@ -1469,12 +1469,12 @@ virtual_heatchill:
- x
- y
- z
title: Point
title: position
type: object
required:
- position
- orientation
title: Pose
title: pose
type: object
sample_id:
type: string
@@ -1491,7 +1491,7 @@ virtual_heatchill:
- pose
- config
- data
title: Resource
title: vessel
type: object
required:
- vessel
@@ -1610,7 +1610,7 @@ virtual_heatchill:
- y
- z
- w
title: Quaternion
title: orientation
type: object
position:
properties:
@@ -1624,12 +1624,12 @@ virtual_heatchill:
- x
- y
- z
title: Point
title: position
type: object
required:
- position
- orientation
title: Pose
title: pose
type: object
sample_id:
type: string
@@ -1646,7 +1646,7 @@ virtual_heatchill:
- pose
- config
- data
title: Resource
title: vessel
type: object
required:
- vessel
@@ -1747,7 +1747,7 @@ virtual_heatchill:
- y
- z
- w
title: Quaternion
title: orientation
type: object
position:
properties:
@@ -1761,12 +1761,12 @@ virtual_heatchill:
- x
- y
- z
title: Point
title: position
type: object
required:
- position
- orientation
title: Pose
title: pose
type: object
sample_id:
type: string
@@ -1783,7 +1783,7 @@ virtual_heatchill:
- pose
- config
- data
title: Resource
title: vessel
type: object
required:
- vessel
@@ -2397,7 +2397,7 @@ virtual_rotavap:
required:
- sec
- nanosec
title: Duration
title: time_remaining
type: object
time_spent:
properties:
@@ -2412,7 +2412,7 @@ virtual_rotavap:
required:
- sec
- nanosec
title: Duration
title: time_spent
type: object
required:
- status
@@ -2468,7 +2468,7 @@ virtual_rotavap:
- y
- z
- w
title: Quaternion
title: orientation
type: object
position:
properties:
@@ -2482,12 +2482,12 @@ virtual_rotavap:
- x
- y
- z
title: Point
title: position
type: object
required:
- position
- orientation
title: Pose
title: pose
type: object
sample_id:
type: string
@@ -2504,7 +2504,7 @@ virtual_rotavap:
- pose
- config
- data
title: Resource
title: vessel
type: object
required:
- vessel
@@ -2889,7 +2889,7 @@ virtual_separator:
- y
- z
- w
title: Quaternion
title: orientation
type: object
position:
properties:
@@ -2903,12 +2903,12 @@ virtual_separator:
- x
- y
- z
title: Point
title: position
type: object
required:
- position
- orientation
title: Pose
title: pose
type: object
sample_id:
type: string
@@ -2925,7 +2925,7 @@ virtual_separator:
- pose
- config
- data
title: Resource
title: from_vessel
type: object
product_phase:
type: string
@@ -2964,7 +2964,7 @@ virtual_separator:
- y
- z
- w
title: Quaternion
title: orientation
type: object
position:
properties:
@@ -2978,12 +2978,12 @@ virtual_separator:
- x
- y
- z
title: Point
title: position
type: object
required:
- position
- orientation
title: Pose
title: pose
type: object
sample_id:
type: string
@@ -3000,7 +3000,7 @@ virtual_separator:
- pose
- config
- data
title: Resource
title: product_vessel
type: object
purpose:
type: string
@@ -3043,7 +3043,7 @@ virtual_separator:
- y
- z
- w
title: Quaternion
title: orientation
type: object
position:
properties:
@@ -3057,12 +3057,12 @@ virtual_separator:
- x
- y
- z
title: Point
title: position
type: object
required:
- position
- orientation
title: Pose
title: pose
type: object
sample_id:
type: string
@@ -3079,7 +3079,7 @@ virtual_separator:
- pose
- config
- data
title: Resource
title: separation_vessel
type: object
settling_time:
type: number
@@ -3128,7 +3128,7 @@ virtual_separator:
- y
- z
- w
title: Quaternion
title: orientation
type: object
position:
properties:
@@ -3142,12 +3142,12 @@ virtual_separator:
- x
- y
- z
title: Point
title: position
type: object
required:
- position
- orientation
title: Pose
title: pose
type: object
sample_id:
type: string
@@ -3164,7 +3164,7 @@ virtual_separator:
- pose
- config
- data
title: Resource
title: to_vessel
type: object
vessel:
properties:
@@ -3201,7 +3201,7 @@ virtual_separator:
- y
- z
- w
title: Quaternion
title: orientation
type: object
position:
properties:
@@ -3215,12 +3215,12 @@ virtual_separator:
- x
- y
- z
title: Point
title: position
type: object
required:
- position
- orientation
title: Pose
title: pose
type: object
sample_id:
type: string
@@ -3237,7 +3237,7 @@ virtual_separator:
- pose
- config
- data
title: Resource
title: vessel
type: object
volume:
type: string
@@ -3276,7 +3276,7 @@ virtual_separator:
- y
- z
- w
title: Quaternion
title: orientation
type: object
position:
properties:
@@ -3290,12 +3290,12 @@ virtual_separator:
- x
- y
- z
title: Point
title: position
type: object
required:
- position
- orientation
title: Pose
title: pose
type: object
sample_id:
type: string
@@ -3312,7 +3312,7 @@ virtual_separator:
- pose
- config
- data
title: Resource
title: waste_phase_to_vessel
type: object
waste_vessel:
properties:
@@ -3349,7 +3349,7 @@ virtual_separator:
- y
- z
- w
title: Quaternion
title: orientation
type: object
position:
properties:
@@ -3363,12 +3363,12 @@ virtual_separator:
- x
- y
- z
title: Point
title: position
type: object
required:
- position
- orientation
title: Pose
title: pose
type: object
sample_id:
type: string
@@ -3385,7 +3385,7 @@ virtual_separator:
- pose
- config
- data
title: Resource
title: waste_vessel
type: object
required:
- vessel
@@ -3951,7 +3951,7 @@ virtual_solid_dispenser:
- y
- z
- w
title: Quaternion
title: orientation
type: object
position:
properties:
@@ -3965,12 +3965,12 @@ virtual_solid_dispenser:
- x
- y
- z
title: Point
title: position
type: object
required:
- position
- orientation
title: Pose
title: pose
type: object
sample_id:
type: string
@@ -3987,7 +3987,7 @@ virtual_solid_dispenser:
- pose
- config
- data
title: Resource
title: vessel
type: object
viscous:
type: boolean
@@ -4332,7 +4332,7 @@ virtual_stirrer:
- y
- z
- w
title: Quaternion
title: orientation
type: object
position:
properties:
@@ -4346,12 +4346,12 @@ virtual_stirrer:
- x
- y
- z
title: Point
title: position
type: object
required:
- position
- orientation
title: Pose
title: pose
type: object
sample_id:
type: string
@@ -4368,7 +4368,7 @@ virtual_stirrer:
- pose
- config
- data
title: Resource
title: vessel
type: object
required:
- vessel
@@ -4492,7 +4492,7 @@ virtual_stirrer:
- y
- z
- w
title: Quaternion
title: orientation
type: object
position:
properties:
@@ -4506,12 +4506,12 @@ virtual_stirrer:
- x
- y
- z
title: Point
title: position
type: object
required:
- position
- orientation
title: Pose
title: pose
type: object
sample_id:
type: string
@@ -4528,7 +4528,7 @@ virtual_stirrer:
- pose
- config
- data
title: Resource
title: vessel
type: object
required:
- vessel
@@ -4639,7 +4639,7 @@ virtual_stirrer:
- y
- z
- w
title: Quaternion
title: orientation
type: object
position:
properties:
@@ -4653,12 +4653,12 @@ virtual_stirrer:
- x
- y
- z
title: Point
title: position
type: object
required:
- position
- orientation
title: Pose
title: pose
type: object
sample_id:
type: string
@@ -4675,7 +4675,7 @@ virtual_stirrer:
- pose
- config
- data
title: Resource
title: vessel
type: object
required:
- vessel
@@ -4704,6 +4704,7 @@ virtual_stirrer:
status_types:
current_speed: float
current_vessel: str
device_info: dict
is_stirring: bool
max_speed: float
min_speed: float
@@ -4738,6 +4739,8 @@ virtual_stirrer:
type: number
current_vessel:
type: string
device_info:
type: object
is_stirring:
type: boolean
max_speed:
@@ -4759,6 +4762,7 @@ virtual_stirrer:
- remaining_time
- max_speed
- min_speed
- device_info
type: object
version: 1.0.0
virtual_transfer_pump:

View File

@@ -24,12 +24,11 @@ DEFAULT_PATHS = [Path(__file__).absolute().parent]
class Registry:
def __init__(self, registry_paths=None):
import ctypes
try:
import unilabos_msgs
except ImportError:
logger.error(
"[UniLab Registry] unilabos_msgs模块未找到请确保已根据官方文档安装unilabos_msgs包。"
)
logger.error("[UniLab Registry] unilabos_msgs模块未找到请确保已根据官方文档安装unilabos_msgs包。")
sys.exit(1)
try:
ctypes.CDLL(str(Path(unilabos_msgs.__file__).parent / "unilabos_msgs_s__rosidl_typesupport_c.pyd"))
@@ -219,7 +218,7 @@ class Registry:
yaml.dump(complete_data, f, allow_unicode=True, default_flow_style=False, Dumper=NoAliasDumper)
self.resource_type_registry.update(data)
logger.trace(
logger.trace( # type: ignore
f"[UniLab Registry] Resource-{current_resource_number} File-{i+1}/{len(files)} "
+ f"Add {list(data.keys())}"
)
@@ -406,7 +405,7 @@ class Registry:
devices_path = abs_path / "devices"
device_comms_path = abs_path / "device_comms"
files = list(devices_path.glob("*.yaml")) + list(device_comms_path.glob("*.yaml"))
logger.trace(
logger.trace( # type: ignore
f"[UniLab Registry] devices: {devices_path.exists()}, device_comms: {device_comms_path.exists()}, "
+ f"total: {len(files)}"
)
@@ -577,7 +576,7 @@ class Registry:
}
device_config["file_path"] = str(file.absolute()).replace("\\", "/")
device_config["registry_type"] = "device"
logger.trace(
logger.trace( # type: ignore
f"[UniLab Registry] Device-{current_device_number} File-{i+1}/{len(files)} Add {device_id} "
+ f"[{data[device_id].get('name', '未命名设备')}]"
)

View File

@@ -1,12 +1,12 @@
get_workstation_plate_resource:
category:
- workstation
class:
module: unilabos.ros.nodes.presets.workstation:get_workstation_plate_resource
type: pylabrobot
description: workstation example resource
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
#get_workstation_plate_resource:
# category:
# - workstation
# class:
# module: unilabos.devices.workstation.workstation_base:get_workstation_plate_resource
# type: pylabrobot
# description: workstation example resource
# handles: []
# icon: ''
# init_param_schema: {}
# registry_type: resource
# version: 1.0.0

View File

@@ -510,7 +510,7 @@ def convert_from_ros_msg_with_mapping(ros_msg: Any, value_mapping: Dict[str, str
Python字典
"""
data: Dict[str, Any] = {}
# # 🔧 添加调试信息
# print(f"🔍 convert_from_ros_msg_with_mapping 开始")
# print(f"🔍 ros_msg 类型: {type(ros_msg)}")
@@ -519,14 +519,14 @@ def convert_from_ros_msg_with_mapping(ros_msg: Any, value_mapping: Dict[str, str
# print("-" * 60)
for msg_name, attr_name in value_mapping.items():
# print(f"🔍 处理映射: {msg_name} -> {attr_name}")
# print(f"🔍 处理映射: {msg_name} -> {attr_name}")
msg_path = msg_name.split(".")
current = ros_msg
# print(f"🔍 msg_path: {msg_path}")
# print(f"🔍 current 初始值: {current} (类型: {type(current)})")
try:
if not attr_name.endswith("[]"):
# 处理单值映射
@@ -539,7 +539,7 @@ def convert_from_ros_msg_with_mapping(ros_msg: Any, value_mapping: Dict[str, str
else:
# print(f"❌ 属性 '{name}' 不存在于 {type(current)}")
break
converted_value = convert_from_ros_msg(current)
# print(f"🔍 转换后的值: {converted_value} (类型: {type(converted_value)})")
data[attr_name] = converted_value
@@ -587,13 +587,13 @@ def convert_from_ros_msg_with_mapping(ros_msg: Any, value_mapping: Dict[str, str
# print(f"❌ 映射转换错误 {msg_name} -> {attr_name}: {e}")
logger.debug(f"Mapping conversion error for {msg_name} -> {attr_name}")
continue
# print(f"🔍 当前 data 状态: {data}")
# print("-" * 40)
#print(f"🔍 convert_from_ros_msg_with_mapping 结束")
#print(f"🔍 最终 data: {data}")
#print("=" * 60)
# print(f"🔍 convert_from_ros_msg_with_mapping 结束")
# print(f"🔍 最终 data: {data}")
# print("=" * 60)
return data
@@ -648,25 +648,28 @@ basic_type_map = {
}
def ros_field_type_to_json_schema(type_info: Type | str, slot_type: str = None) -> Dict[str, Any]:
def ros_field_type_to_json_schema(
type_info: Type | str, field_name: str
) -> Dict[str, Any]:
"""
将 ROS 字段类型转换为 JSON Schema 类型定义
Args:
type_info: ROS 类型
slot_type: ROS 类型
field_name: 字段名用于设置复杂类型的title
Returns:
对应的 JSON Schema 类型定义
"""
if isinstance(type_info, UnboundedSequence):
return {"type": "array", "items": ros_field_type_to_json_schema(type_info.value_type)}
return {"type": "array", "items": ros_field_type_to_json_schema(type_info.value_type, field_name)} # type: ignore
if isinstance(type_info, NamespacedType):
cls_name = ".".join(type_info.namespaces) + ":" + type_info.name
type_class = msg_converter_manager.get_class(cls_name)
return ros_field_type_to_json_schema(type_class)
return ros_field_type_to_json_schema(type_class, field_name)
elif isinstance(type_info, BasicType):
return ros_field_type_to_json_schema(type_info.typename)
return ros_field_type_to_json_schema(type_info.typename, field_name)
elif isinstance(type_info, UnboundedString):
return basic_type_map["string"]
elif isinstance(type_info, str):
@@ -683,8 +686,9 @@ def ros_field_type_to_json_schema(type_info: Type | str, slot_type: str = None)
},
"required": ["sec", "nanosec"],
}
return {}
else:
return ros_message_to_json_schema(type_info)
return ros_message_to_json_schema(type_info, field_name)
# # 处理数组类型
# if field_type.endswith('[]'):
# item_type = field_type[:-2]
@@ -708,28 +712,28 @@ def ros_field_type_to_json_schema(type_info: Type | str, slot_type: str = None)
# return {'type': 'object', 'description': f'未知类型: {field_type}'}
def ros_message_to_json_schema(msg_class: Any) -> Dict[str, Any]:
def ros_message_to_json_schema(msg_class: Any, field_name: str) -> Dict[str, Any]:
"""
将 ROS 消息类转换为 JSON Schema
Args:
msg_class: ROS 消息类
field_name: 字段名用于设置schema的title如果为None则使用类名
Returns:
对应的 JSON Schema 定义
"""
schema = {"type": "object", "properties": {}, "required": []}
# 获取类名作为标题
if hasattr(msg_class, "__name__"):
schema["title"] = msg_class.__name__
# 优先使用字段名作为标题,否则使用类名
schema["title"] = field_name
# 获取消息的字段和字段类型
try:
for ind, slot_info in enumerate(msg_class._fields_and_field_types.items()):
slot_name, slot_type = slot_info
type_info = msg_class.SLOT_TYPES[ind]
field_schema = ros_field_type_to_json_schema(type_info, slot_type)
field_schema = ros_field_type_to_json_schema(type_info, slot_name)
schema["properties"][slot_name] = field_schema
schema["required"].append(slot_name)
# if hasattr(msg_class, 'get_fields_and_field_types'):
@@ -788,15 +792,15 @@ def ros_action_to_json_schema(action_class: Any, description="") -> Dict[str, An
"properties": {
"goal": {
# 'description': 'Action 目标 - 从客户端发送到服务器',
**ros_message_to_json_schema(action_class.Goal)
**ros_message_to_json_schema(action_class.Goal, action_class.Goal.__name__)
},
"feedback": {
# 'description': 'Action 反馈 - 执行过程中从服务器发送到客户端',
**ros_message_to_json_schema(action_class.Feedback)
**ros_message_to_json_schema(action_class.Feedback, action_class.Feedback.__name__)
},
"result": {
# 'description': 'Action 结果 - 完成后从服务器发送到客户端',
**ros_message_to_json_schema(action_class.Result)
**ros_message_to_json_schema(action_class.Result, action_class.Result.__name__)
},
},
"required": ["goal"],