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

588
example_devices.py Normal file
View File

@@ -0,0 +1,588 @@
"""
示例设备类文件,用于测试注册表编辑器
"""
import asyncio
from typing import Dict, Any, Optional, List
class SmartPumpController:
"""
智能泵控制器
支持多种泵送模式,具有高精度流量控制和自动校准功能。
适用于实验室自动化系统中的液体处理任务。
"""
def __init__(self, device_id: str = "smart_pump_01", port: str = "/dev/ttyUSB0"):
"""
初始化智能泵控制器
Args:
device_id: 设备唯一标识符
port: 通信端口
"""
self.device_id = device_id
self.port = port
self.is_connected = False
self.current_flow_rate = 0.0
self.total_volume_pumped = 0.0
self.calibration_factor = 1.0
self.pump_mode = "continuous" # continuous, volume, rate
def connect_device(self, timeout: int = 10) -> bool:
"""
连接到泵设备
Args:
timeout: 连接超时时间(秒)
Returns:
bool: 连接是否成功
"""
# 模拟连接过程
self.is_connected = True
return True
def disconnect_device(self) -> bool:
"""
断开设备连接
Returns:
bool: 断开连接是否成功
"""
self.is_connected = False
self.current_flow_rate = 0.0
return True
def set_flow_rate(self, flow_rate: float, units: str = "ml/min") -> bool:
"""
设置泵流速
Args:
flow_rate: 流速值
units: 流速单位
Returns:
bool: 设置是否成功
"""
if not self.is_connected:
return False
self.current_flow_rate = flow_rate
return True
async def pump_volume_async(self, volume: float, flow_rate: float) -> Dict[str, Any]:
"""
异步泵送指定体积的液体
Args:
volume: 目标体积 (mL)
flow_rate: 泵送流速 (mL/min)
Returns:
Dict: 包含操作结果的字典
"""
if not self.is_connected:
return {"success": False, "error": "设备未连接"}
# 计算泵送时间
pump_time = (volume / flow_rate) * 60 # 转换为秒
self.current_flow_rate = flow_rate
await asyncio.sleep(min(pump_time, 3.0)) # 模拟泵送过程
self.total_volume_pumped += volume
self.current_flow_rate = 0.0
return {
"success": True,
"pumped_volume": volume,
"actual_time": min(pump_time, 3.0),
"total_volume": self.total_volume_pumped,
}
def emergency_stop(self) -> bool:
"""
紧急停止泵
Returns:
bool: 停止是否成功
"""
self.current_flow_rate = 0.0
return True
def perform_calibration(self, reference_volume: float, measured_volume: float) -> bool:
"""
执行泵校准
Args:
reference_volume: 参考体积
measured_volume: 实际测量体积
Returns:
bool: 校准是否成功
"""
if measured_volume > 0:
self.calibration_factor = reference_volume / measured_volume
return True
return False
# 状态查询方法
def get_connection_status(self) -> str:
"""获取连接状态"""
return "connected" if self.is_connected else "disconnected"
def get_current_flow_rate(self) -> float:
"""获取当前流速 (mL/min)"""
return self.current_flow_rate
def get_total_volume(self) -> float:
"""获取累计泵送体积 (mL)"""
return self.total_volume_pumped
def get_calibration_factor(self) -> float:
"""获取校准因子"""
return self.calibration_factor
def get_pump_mode(self) -> str:
"""获取泵送模式"""
return self.pump_mode
def get_device_status(self) -> Dict[str, Any]:
"""获取设备完整状态信息"""
return {
"device_id": self.device_id,
"connected": self.is_connected,
"flow_rate": self.current_flow_rate,
"total_volume": self.total_volume_pumped,
"calibration_factor": self.calibration_factor,
"mode": self.pump_mode,
"running": self.current_flow_rate > 0,
}
class AdvancedTemperatureController:
"""
高级温度控制器
支持PID控制、多点温度监控和程序化温度曲线。
适用于需要精确温度控制的化学反应和材料处理过程。
"""
def __init__(self, controller_id: str = "temp_controller_01"):
"""
初始化温度控制器
Args:
controller_id: 控制器ID
"""
self.controller_id = controller_id
self.current_temperature = 25.0
self.target_temperature = 25.0
self.is_heating = False
self.is_cooling = False
self.pid_enabled = True
self.temperature_history: List[Dict] = []
def set_target_temperature(self, temperature: float, rate: float = 10.0) -> bool:
"""
设置目标温度
Args:
temperature: 目标温度 (°C)
rate: 升温/降温速率 (°C/min)
Returns:
bool: 设置是否成功
"""
self.target_temperature = temperature
return True
async def heat_to_temperature_async(
self, temperature: float, tolerance: float = 0.5, timeout: int = 600
) -> Dict[str, Any]:
"""
异步加热到指定温度
Args:
temperature: 目标温度 (°C)
tolerance: 温度容差 (°C)
timeout: 最大等待时间 (秒)
Returns:
Dict: 操作结果
"""
self.target_temperature = temperature
start_temp = self.current_temperature
if temperature > start_temp:
self.is_heating = True
elif temperature < start_temp:
self.is_cooling = True
# 模拟温度变化过程
steps = min(abs(temperature - start_temp) * 2, 20) # 计算步数
step_time = min(timeout / steps if steps > 0 else 1, 2.0) # 每步最多2秒
for step in range(int(steps)):
progress = (step + 1) / steps
self.current_temperature = start_temp + (temperature - start_temp) * progress
# 记录温度历史
self.temperature_history.append(
{
"timestamp": asyncio.get_event_loop().time(),
"temperature": self.current_temperature,
"target": self.target_temperature,
}
)
await asyncio.sleep(step_time)
# 保持历史记录不超过100条
if len(self.temperature_history) > 100:
self.temperature_history.pop(0)
# 最终设置为目标温度
self.current_temperature = temperature
self.is_heating = False
self.is_cooling = False
return {
"success": True,
"final_temperature": self.current_temperature,
"start_temperature": start_temp,
"time_taken": steps * step_time,
}
def enable_pid_control(self, kp: float = 1.0, ki: float = 0.1, kd: float = 0.05) -> bool:
"""
启用PID控制
Args:
kp: 比例增益
ki: 积分增益
kd: 微分增益
Returns:
bool: 启用是否成功
"""
self.pid_enabled = True
return True
def run_temperature_program(self, program: List[Dict]) -> bool:
"""
运行温度程序
Args:
program: 温度程序列表,每个元素包含温度和持续时间
Returns:
bool: 程序启动是否成功
"""
# 模拟程序启动
return True
# 状态查询方法
def get_current_temperature(self) -> float:
"""获取当前温度 (°C)"""
return round(self.current_temperature, 2)
def get_target_temperature(self) -> float:
"""获取目标温度 (°C)"""
return self.target_temperature
def get_heating_status(self) -> bool:
"""获取加热状态"""
return self.is_heating
def get_cooling_status(self) -> bool:
"""获取制冷状态"""
return self.is_cooling
def get_pid_status(self) -> bool:
"""获取PID控制状态"""
return self.pid_enabled
def get_temperature_history(self) -> List[Dict]:
"""获取温度历史记录"""
return self.temperature_history[-10:] # 返回最近10条记录
def get_controller_status(self) -> Dict[str, Any]:
"""获取控制器完整状态"""
return {
"controller_id": self.controller_id,
"current_temp": self.current_temperature,
"target_temp": self.target_temperature,
"is_heating": self.is_heating,
"is_cooling": self.is_cooling,
"pid_enabled": self.pid_enabled,
"history_count": len(self.temperature_history),
}
class MultiChannelAnalyzer:
"""
多通道分析仪
支持同时监测多个通道的信号,提供实时数据采集和分析功能。
常用于光谱分析、电化学测量等应用场景。
"""
def __init__(self, analyzer_id: str = "analyzer_01", channels: int = 8):
"""
初始化多通道分析仪
Args:
analyzer_id: 分析仪ID
channels: 通道数量
"""
self.analyzer_id = analyzer_id
self.channel_count = channels
self.channel_data = {i: {"value": 0.0, "unit": "V", "enabled": True} for i in range(channels)}
self.is_measuring = False
self.sample_rate = 1000 # Hz
def configure_channel(self, channel: int, enabled: bool = True, unit: str = "V") -> bool:
"""
配置通道
Args:
channel: 通道编号
enabled: 是否启用
unit: 测量单位
Returns:
bool: 配置是否成功
"""
if 0 <= channel < self.channel_count:
self.channel_data[channel]["enabled"] = enabled
self.channel_data[channel]["unit"] = unit
return True
return False
async def start_measurement_async(self, duration: int = 10) -> Dict[str, Any]:
"""
开始异步测量
Args:
duration: 测量持续时间(秒)
Returns:
Dict: 测量结果
"""
self.is_measuring = True
# 模拟数据采集
measurements = []
for second in range(duration):
timestamp = asyncio.get_event_loop().time()
frame_data = {}
for channel in range(self.channel_count):
if self.channel_data[channel]["enabled"]:
# 模拟传感器数据
import random
value = random.uniform(-5.0, 5.0)
frame_data[f"channel_{channel}"] = value
self.channel_data[channel]["value"] = value
measurements.append({"timestamp": timestamp, "data": frame_data})
await asyncio.sleep(1.0) # 每秒采集一次
self.is_measuring = False
return {
"success": True,
"duration": duration,
"samples_count": len(measurements),
"measurements": measurements[-5:], # 只返回最后5个样本
"channels_active": len([ch for ch in self.channel_data.values() if ch["enabled"]]),
}
def stop_measurement(self) -> bool:
"""
停止测量
Returns:
bool: 停止是否成功
"""
self.is_measuring = False
return True
def reset_channels(self) -> bool:
"""
重置所有通道
Returns:
bool: 重置是否成功
"""
for channel in self.channel_data:
self.channel_data[channel]["value"] = 0.0
return True
# 状态查询方法
def get_measurement_status(self) -> bool:
"""获取测量状态"""
return self.is_measuring
def get_channel_count(self) -> int:
"""获取通道数量"""
return self.channel_count
def get_sample_rate(self) -> float:
"""获取采样率 (Hz)"""
return self.sample_rate
def get_channel_values(self) -> Dict[int, float]:
"""获取所有通道的当前值"""
return {ch: data["value"] for ch, data in self.channel_data.items() if data["enabled"]}
def get_enabled_channels(self) -> List[int]:
"""获取已启用的通道列表"""
return [ch for ch, data in self.channel_data.items() if data["enabled"]]
def get_analyzer_status(self) -> Dict[str, Any]:
"""获取分析仪完整状态"""
return {
"analyzer_id": self.analyzer_id,
"channel_count": self.channel_count,
"is_measuring": self.is_measuring,
"sample_rate": self.sample_rate,
"active_channels": len(self.get_enabled_channels()),
"channel_data": self.channel_data,
}
class AutomatedDispenser:
"""
自动分配器
精确控制固体和液体材料的分配,支持多种分配模式和容器管理。
集成称重功能,确保分配精度和重现性。
"""
def __init__(self, dispenser_id: str = "dispenser_01"):
"""
初始化自动分配器
Args:
dispenser_id: 分配器ID
"""
self.dispenser_id = dispenser_id
self.is_ready = True
self.current_position = {"x": 0.0, "y": 0.0, "z": 0.0}
self.dispensed_total = 0.0
self.container_capacity = 1000.0 # mL
self.precision_mode = True
def move_to_position(self, x: float, y: float, z: float) -> bool:
"""
移动到指定位置
Args:
x: X坐标 (mm)
y: Y坐标 (mm)
z: Z坐标 (mm)
Returns:
bool: 移动是否成功
"""
self.current_position = {"x": x, "y": y, "z": z}
return True
async def dispense_liquid_async(self, volume: float, container_id: str, viscosity: str = "low") -> Dict[str, Any]:
"""
异步分配液体
Args:
volume: 分配体积 (mL)
container_id: 容器ID
viscosity: 液体粘度等级
Returns:
Dict: 分配结果
"""
if not self.is_ready:
return {"success": False, "error": "设备未就绪"}
if volume <= 0:
return {"success": False, "error": "体积必须大于0"}
# 模拟分配过程
dispense_time = volume * 0.1 # 每mL需要0.1秒
if viscosity == "high":
dispense_time *= 2 # 高粘度液体需要更长时间
await asyncio.sleep(min(dispense_time, 5.0)) # 最多等待5秒
self.dispensed_total += volume
return {
"success": True,
"dispensed_volume": volume,
"container_id": container_id,
"actual_time": min(dispense_time, 5.0),
"total_dispensed": self.dispensed_total,
}
def clean_dispenser(self, wash_volume: float = 5.0) -> bool:
"""
清洗分配器
Args:
wash_volume: 清洗液体积 (mL)
Returns:
bool: 清洗是否成功
"""
# 模拟清洗过程
return True
def calibrate_volume(self, target_volume: float) -> bool:
"""
校准分配体积
Args:
target_volume: 校准目标体积 (mL)
Returns:
bool: 校准是否成功
"""
# 模拟校准过程
return True
# 状态查询方法
def get_ready_status(self) -> bool:
"""获取就绪状态"""
return self.is_ready
def get_current_position(self) -> Dict[str, float]:
"""获取当前位置坐标"""
return self.current_position.copy()
def get_dispensed_total(self) -> float:
"""获取累计分配体积 (mL)"""
return self.dispensed_total
def get_container_capacity(self) -> float:
"""获取容器容量 (mL)"""
return self.container_capacity
def get_precision_mode(self) -> bool:
"""获取精密模式状态"""
return self.precision_mode
def get_dispenser_status(self) -> Dict[str, Any]:
"""获取分配器完整状态"""
return {
"dispenser_id": self.dispenser_id,
"ready": self.is_ready,
"position": self.current_position,
"dispensed_total": self.dispensed_total,
"capacity": self.container_capacity,
"precision_mode": self.precision_mode,
}

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页面 HTMLResponse: 渲染后的HTML页面
""" """
try: try:
# 准备设备数据 # 准备初始数据结构这些数据将通过WebSocket实时更新
devices = [] devices = []
resources = [] resources = []
modules = {"names": [], "classes": [], "displayed_count": 0, "total_count": 0} modules = {"names": [], "classes": [], "displayed_count": 0, "total_count": 0}
# 获取在线设备信息 # 获取在线设备信息(用于初始渲染)
ros_node_info = get_ros_node_info() ros_node_info = get_ros_node_info()
# 获取主机节点信息 # 获取主机节点信息(用于初始渲染)
host_node_info = get_host_node_info() host_node_info = get_host_node_info()
# 获取Registry路径信息 # 获取Registry路径信息(静态信息,不需要实时更新)
registry_info = get_registry_info() registry_info = get_registry_info()
# 获取已加载的设备 # 获取初始数据用于页面渲染后续将被WebSocket数据覆盖
if lab_registry: 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(): for resource_id, resource_info in lab_registry.resource_type_registry.items():
resources.append( resources.append(
@@ -103,7 +105,7 @@ def setup_web_pages(router: APIRouter) -> None:
} }
) )
# 获取导入的模块 # 获取导入的模块(初始数据)
if msg_converter_manager: if msg_converter_manager:
modules["names"] = msg_converter_manager.list_modules() modules["names"] = msg_converter_manager.list_modules()
all_classes = [i for i in msg_converter_manager.list_classes() if "." in i] 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: except Exception as e:
error(f"打开文件夹时出错: {str(e)}") error(f"打开文件夹时出错: {str(e)}")
return {"status": "error", "message": f"Failed to open folder: {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> <body>
<h1>{% block header %}UniLab{% endblock %}</h1> <h1>{% block header %}UniLab{% endblock %}</h1>
{% block nav %} {% block nav %}
<a href="/unilabos/webtic" class="home-link">Home</a>
{% endblock %} {% endblock %}
{% block top_info %}{% endblock %} {% block top_info %}{% endblock %}

View File

@@ -1,22 +1,25 @@
{% extends "base.html" %} {% extends "base.html" %} {% block title %}UniLab API{% endblock %} {% block
header %}UniLab API{% endblock %} {% block nav %}
{% block title %}UniLab API{% endblock %} <div class="nav-tabs">
<a
{% block header %}UniLab API{% endblock %} href="/"
class="nav-tab"
{% block nav %} style="background-color: #2196f3; color: white"
<a href="/status" class="status-link">System Status</a> target="_blank"
{% endblock %} >主页</a
>
{% block content %} <a href="/status" class="nav-tab">状态</a>
<div class="card"> <a href="/registry-editor" class="nav-tab" target="_blank">注册表编辑</a>
<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> </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 - y
- z - z
- w - w
title: Quaternion title: orientation
type: object type: object
position: position:
properties: properties:
@@ -350,12 +350,12 @@ separator.homemade:
- x - x
- y - y
- z - z
title: Point title: position
type: object type: object
required: required:
- position - position
- orientation - orientation
title: Pose title: pose
type: object type: object
sample_id: sample_id:
type: string type: string
@@ -372,7 +372,7 @@ separator.homemade:
- pose - pose
- config - config
- data - data
title: Resource title: vessel
type: object type: object
required: required:
- vessel - vessel

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
get_workstation_plate_resource: #get_workstation_plate_resource:
category: # category:
- workstation # - workstation
class: # class:
module: unilabos.ros.nodes.presets.workstation:get_workstation_plate_resource # module: unilabos.devices.workstation.workstation_base:get_workstation_plate_resource
type: pylabrobot # type: pylabrobot
description: workstation example resource # description: workstation example resource
handles: [] # handles: []
icon: '' # icon: ''
init_param_schema: {} # init_param_schema: {}
registry_type: resource # registry_type: resource
version: 1.0.0 # 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字典 Python字典
""" """
data: Dict[str, Any] = {} data: Dict[str, Any] = {}
# # 🔧 添加调试信息 # # 🔧 添加调试信息
# print(f"🔍 convert_from_ros_msg_with_mapping 开始") # print(f"🔍 convert_from_ros_msg_with_mapping 开始")
# print(f"🔍 ros_msg 类型: {type(ros_msg)}") # 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) # print("-" * 60)
for msg_name, attr_name in value_mapping.items(): 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(".") msg_path = msg_name.split(".")
current = ros_msg current = ros_msg
# print(f"🔍 msg_path: {msg_path}") # print(f"🔍 msg_path: {msg_path}")
# print(f"🔍 current 初始值: {current} (类型: {type(current)})") # print(f"🔍 current 初始值: {current} (类型: {type(current)})")
try: try:
if not attr_name.endswith("[]"): 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: else:
# print(f"❌ 属性 '{name}' 不存在于 {type(current)}") # print(f"❌ 属性 '{name}' 不存在于 {type(current)}")
break break
converted_value = convert_from_ros_msg(current) converted_value = convert_from_ros_msg(current)
# print(f"🔍 转换后的值: {converted_value} (类型: {type(converted_value)})") # print(f"🔍 转换后的值: {converted_value} (类型: {type(converted_value)})")
data[attr_name] = 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}") # print(f"❌ 映射转换错误 {msg_name} -> {attr_name}: {e}")
logger.debug(f"Mapping conversion error for {msg_name} -> {attr_name}") logger.debug(f"Mapping conversion error for {msg_name} -> {attr_name}")
continue continue
# print(f"🔍 当前 data 状态: {data}") # print(f"🔍 当前 data 状态: {data}")
# print("-" * 40) # print("-" * 40)
#print(f"🔍 convert_from_ros_msg_with_mapping 结束") # print(f"🔍 convert_from_ros_msg_with_mapping 结束")
#print(f"🔍 最终 data: {data}") # print(f"🔍 最终 data: {data}")
#print("=" * 60) # print("=" * 60)
return data 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 类型定义 将 ROS 字段类型转换为 JSON Schema 类型定义
Args: Args:
type_info: ROS 类型 type_info: ROS 类型
slot_type: ROS 类型 slot_type: ROS 类型
field_name: 字段名用于设置复杂类型的title
Returns: Returns:
对应的 JSON Schema 类型定义 对应的 JSON Schema 类型定义
""" """
if isinstance(type_info, UnboundedSequence): 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): if isinstance(type_info, NamespacedType):
cls_name = ".".join(type_info.namespaces) + ":" + type_info.name cls_name = ".".join(type_info.namespaces) + ":" + type_info.name
type_class = msg_converter_manager.get_class(cls_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): 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): elif isinstance(type_info, UnboundedString):
return basic_type_map["string"] return basic_type_map["string"]
elif isinstance(type_info, str): 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"], "required": ["sec", "nanosec"],
} }
return {}
else: else:
return ros_message_to_json_schema(type_info) return ros_message_to_json_schema(type_info, field_name)
# # 处理数组类型 # # 处理数组类型
# if field_type.endswith('[]'): # if field_type.endswith('[]'):
# item_type = field_type[:-2] # 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}'} # 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 将 ROS 消息类转换为 JSON Schema
Args: Args:
msg_class: ROS 消息类 msg_class: ROS 消息类
field_name: 字段名用于设置schema的title如果为None则使用类名
Returns: Returns:
对应的 JSON Schema 定义 对应的 JSON Schema 定义
""" """
schema = {"type": "object", "properties": {}, "required": []} schema = {"type": "object", "properties": {}, "required": []}
# 获取类名作为标题 # 优先使用字段名作为标题,否则使用类名
if hasattr(msg_class, "__name__"): schema["title"] = field_name
schema["title"] = msg_class.__name__
# 获取消息的字段和字段类型 # 获取消息的字段和字段类型
try: try:
for ind, slot_info in enumerate(msg_class._fields_and_field_types.items()): for ind, slot_info in enumerate(msg_class._fields_and_field_types.items()):
slot_name, slot_type = slot_info slot_name, slot_type = slot_info
type_info = msg_class.SLOT_TYPES[ind] 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["properties"][slot_name] = field_schema
schema["required"].append(slot_name) schema["required"].append(slot_name)
# if hasattr(msg_class, 'get_fields_and_field_types'): # 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": { "properties": {
"goal": { "goal": {
# 'description': 'Action 目标 - 从客户端发送到服务器', # 'description': 'Action 目标 - 从客户端发送到服务器',
**ros_message_to_json_schema(action_class.Goal) **ros_message_to_json_schema(action_class.Goal, action_class.Goal.__name__)
}, },
"feedback": { "feedback": {
# 'description': 'Action 反馈 - 执行过程中从服务器发送到客户端', # 'description': 'Action 反馈 - 执行过程中从服务器发送到客户端',
**ros_message_to_json_schema(action_class.Feedback) **ros_message_to_json_schema(action_class.Feedback, action_class.Feedback.__name__)
}, },
"result": { "result": {
# 'description': 'Action 结果 - 完成后从服务器发送到客户端', # 'description': 'Action 结果 - 完成后从服务器发送到客户端',
**ros_message_to_json_schema(action_class.Result) **ros_message_to_json_schema(action_class.Result, action_class.Result.__name__)
}, },
}, },
"required": ["goal"], "required": ["goal"],