Dev v0.9.0 (#23)

Add high-level PLR functions
Add Laiyu/Zhida driver support
Fix ROS node discovery issues
Add hostname and resource query support
Fix ROS message conversion logic
Support configuration via environment variables

* Update README and MQTTClient for installation instructions and code improvements

* feat: 支持local_config启动
add: 增加对crt path的说明,为传入config.py的相对路径
move: web component

* add: registry description

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* feat: node_info_update srv
fix: OTDeck cant create

* close #12
feat: slave node registry

* feat: show machine name
fix: host node registry not uploaded

* feat: add hplc registry

* feat: add hplc registry

* fix: hplc status typo

* fix: devices/

* 完成启动OT并联动rviz

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* fix: device.class possible null

* fix: HPLC additions with online service

* fix: slave mode spin not working

* fix: slave mode spin not working

* 修复rviz位置问题,

修复rviz位置问题,
在无tf变动时减缓发送频率
在backend中添加物料跟随方法

* feat: 多ProtocolNode 允许子设备ID相同
feat: 上报发现的ActionClient
feat: Host重启动,通过discover机制要求slaveNode重新注册,实现信息及时上报

* feat: 支持env设置config

* fix: running logic

* fix: running logic

* fix: missing ot

* 在main中直接初始化republisher和物料的mesh节点

* 将joint_republisher和resource_mesh_manager添加进 main_slave_run.py中

* Device visualization (#14)

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* 修复rviz位置问题,

修复rviz位置问题,
在无tf变动时减缓发送频率
在backend中添加物料跟随方法

* fix: running logic

* fix: running logic

* fix: missing ot

* 在main中直接初始化republisher和物料的mesh节点

* 将joint_republisher和resource_mesh_manager添加进 main_slave_run.py中

---------

Co-authored-by: zhangshixiang <@zhangshixiang>
Co-authored-by: wznln <18435084+Xuwznln@users.noreply.github.com>

* fix: missing hostname in devices_names
fix: upload_file for model file

* fix: missing paho-mqtt package
bump version to 0.9.0

* fix startup
add ResourceCreateFromOuter.action

* fix type hint

* update actions

* update actions

* host node add_resource_from_outer
fix cmake list

* pass device config to device class

* add: bind_parent_ids to resource create action
fix: message convert string

* fix: host node should not be re_discovered

* feat: resource tracker support dict

* feat: add more necessary params

* feat: fix boolean null in registry action data

* feat: add outer resource

* 编写mesh添加action

* feat: append resource

* add action

* feat: vis 2d for plr

* fix

* fix: browser on rviz

* fix: cloud bridge error fallback to local

* fix: salve auto run rviz

* 初始化两个plate

* Device visualization (#22)

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* 修复rviz位置问题,

修复rviz位置问题,
在无tf变动时减缓发送频率
在backend中添加物料跟随方法

* fix: running logic

* fix: running logic

* fix: missing ot

* 在main中直接初始化republisher和物料的mesh节点

* 将joint_republisher和resource_mesh_manager添加进 main_slave_run.py中

* 编写mesh添加action

* add action

* fix

* fix: browser on rviz

* fix: cloud bridge error fallback to local

* fix: salve auto run rviz

* 初始化两个plate

---------

Co-authored-by: zhangshixiang <@zhangshixiang>
Co-authored-by: wznln <18435084+Xuwznln@users.noreply.github.com>

* fix: multi channel

* fix: aspirate

* fix: aspirate

* fix: aspirate

* fix: aspirate

* 提交

* fix: jobadd

* fix: jobadd

* fix: msg converter

* tijiao

---------

Co-authored-by: Harvey Que <Q-Query@outlook.com>
Co-authored-by: zhangshixiang <@zhangshixiang>
Co-authored-by: q434343 <73513873+q434343@users.noreply.github.com>
This commit is contained in:
Xuwznln
2025-05-07 17:37:03 +08:00
committed by GitHub
parent d1fbea3b7d
commit 5a564c0c05
93 changed files with 13081 additions and 211 deletions

View File

@@ -42,10 +42,11 @@ conda env update --file unilabos-[YOUR_OS].yml -n 环境名
# 现阶段,需要安装 `unilabos_msgs` 包 # 现阶段,需要安装 `unilabos_msgs` 包
# 可以前往 Release 页面下载系统对应的包进行安装 # 可以前往 Release 页面下载系统对应的包进行安装
conda install ros-humble-unilabos-msgs-0.8.0-xxxxx.tar.bz2 conda install ros-humble-unilabos-msgs-0.9.0-xxxxx.tar.bz2
# 安装PyLabRobot等前置 # 安装PyLabRobot等前置
git clone https://github.com/PyLabRobot/pylabrobot git clone https://github.com/PyLabRobot/pylabrobot plr_repo
cd plr_repo
pip install .[opentrons] pip install .[opentrons]
``` ```

View File

@@ -1,6 +1,6 @@
package: package:
name: ros-humble-unilabos-msgs name: ros-humble-unilabos-msgs
version: 0.8.0 version: 0.9.0
source: source:
path: ../../unilabos_msgs path: ../../unilabos_msgs
folder: ros-humble-unilabos-msgs/src/work folder: ros-humble-unilabos-msgs/src/work

View File

@@ -1,6 +1,6 @@
package: package:
name: unilabos name: unilabos
version: "0.8.0" version: "0.9.0"
source: source:
path: ../.. path: ../..

View File

@@ -4,7 +4,7 @@ package_name = 'unilabos'
setup( setup(
name=package_name, name=package_name,
version='0.8.0', version='0.9.0',
packages=find_packages(), packages=find_packages(),
include_package_data=True, include_package_data=True,
install_requires=['setuptools'], install_requires=['setuptools'],

View File

@@ -0,0 +1,5 @@
使用plr_test.json启动将Well加入Plate中
```bash
ros2 action send_goal /devices/host_node/add_resource_from_outer unilabos_msgs/action/_resource_create_from_outer/ResourceCreateFromOuter "{ resources: [ { 'category': '', 'children': [], 'config': { 'type': 'Well', 'size_x': 6.86, 'size_y': 6.86, 'size_z': 10.67, 'rotation': { 'x': 0, 'y': 0, 'z': 0, 'type': 'Rotation' }, 'category': 'well', 'model': null, 'max_volume': 360, 'material_z_thickness': 0.5, 'compute_volume_from_height': null, 'compute_height_from_volume': null, 'bottom_type': 'flat', 'cross_section_type': 'circle' }, 'data': { 'liquids': [], 'pending_liquids': [], 'liquid_history': [] }, 'id': 'plate_well_11_7', 'name': 'plate_well_11_7', 'pose': { 'orientation': { 'w': 1.0, 'x': 0.0, 'y': 0.0, 'z': 0.0 }, 'position': { 'x': 0.0, 'y': 0.0, 'z': 0.0 } }, 'sample_id': '', 'parent': 'plate', 'type': 'device' } ], device_ids: [ 'PLR_STATION' ], bind_parent_ids: [ 'plate' ], bind_locations: [ { 'x': 0.0, 'y': 0.0, 'z': 0.0 } ], other_calling_params: [ '{}' ] }"
```

View File

@@ -6679,8 +6679,7 @@
"plate_well_11_3", "plate_well_11_3",
"plate_well_11_4", "plate_well_11_4",
"plate_well_11_5", "plate_well_11_5",
"plate_well_11_6", "plate_well_11_6"
"plate_well_11_7"
], ],
"parent": "deck", "parent": "deck",
"type": "device", "type": "device",
@@ -10508,45 +10507,6 @@
"pending_liquids": [], "pending_liquids": [],
"liquid_history": [] "liquid_history": []
} }
},
{
"id": "plate_well_11_7",
"name": "plate_well_11_7",
"sample_id": null,
"children": [],
"parent": "plate",
"type": "device",
"class": "",
"position": {
"x": 109.87,
"y": 7.77,
"z": 3.03
},
"config": {
"type": "Well",
"size_x": 6.86,
"size_y": 6.86,
"size_z": 10.67,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "well",
"model": null,
"max_volume": 360,
"material_z_thickness": 0.5,
"compute_volume_from_height": null,
"compute_height_from_volume": null,
"bottom_type": "flat",
"cross_section_type": "circle"
},
"data": {
"liquids": [],
"pending_liquids": [],
"liquid_history": []
}
} }
], ],
"links": [] "links": []

File diff suppressed because it is too large Load Diff

View File

@@ -4,13 +4,14 @@
"id": "Gripper1", "id": "Gripper1",
"name": "假夹爪", "name": "假夹爪",
"children": [ "children": [
"Plate1"
], ],
"parent": null, "parent": null,
"type": "device", "type": "device",
"class": "gripper.mock", "class": "gripper.mock",
"position": { "position": {
"x": 620.6111111111111, "x": 0,
"y": 171, "y": 0,
"z": 0 "z": 0
}, },
"config": { "config": {
@@ -23,18 +24,120 @@
"name": "Plate1", "name": "Plate1",
"children": [ "children": [
], ],
"parent": null, "parent": "Gripper1",
"type": "plate", "type": "plate",
"class": "nest_96_wellplate_2ml_deep", "class": "nest_96_wellplate_100ul_pcr_full_skirt",
"position": { "position": {
"x": 620.6111111111111, "x": 0,
"y": 171, "y": 0,
"z": 0 "z": 69
}, },
"config": { "config": {
}, },
"data": { "data": {
} }
},
{
"id": "ot_joint_publisher",
"name": "ot_joint_publisher",
"sample_id": null,
"children": [
],
"parent": null,
"type": "device",
"class": "lh_joint_publisher",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"lh_id":"deck",
"joint_config":
{
"joint_names":[
"first_joint",
"second_joint",
"third_joint",
"fourth_joint"
],
"y":{
"first_joint":{
"factor":-1,
"offset":0.0
}
},
"x":{
"second_joint":{
"factor":-1,
"offset":0.0
}
},
"z":{
"third_joint":{
"factor":1,
"offset":0.0
},
"fourth_joint":{
"factor":1,
"offset":0.0
}
}
}
},
"data": {}
},
{
"id": "ot_joint_publisher",
"name": "ot_joint_publisher",
"sample_id": null,
"children": [
],
"parent": null,
"type": "device",
"class": "lh_joint_publisher",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"lh_id":"deck",
"joint_config":
{
"joint_names":[
"first_joint",
"second_joint",
"third_joint",
"fourth_joint"
],
"y":{
"first_joint":{
"factor":-1,
"offset":0.0
}
},
"x":{
"second_joint":{
"factor":-1,
"offset":0.0
}
},
"z":{
"third_joint":{
"factor":1,
"offset":0.0
},
"fourth_joint":{
"factor":1,
"offset":0.0
}
}
}
},
"data": {}
} }
], ],
"links": [ "links": [

View File

@@ -0,0 +1,135 @@
{
"nodes": [
{
"id": "PLR_STATION",
"name": "PLR_LH_TEST",
"parent": null,
"type": "device",
"class": "liquid_handler",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"data": {
"children": [
{
"_resource_child_name": "deck",
"_resource_type": "pylabrobot.resources.opentrons.deck:OTDeck"
}
],
"backend": {
"type": "LiquidHandlerRvizBackend"
}
}
},
"data": {},
"children": [
"deck"
]
},
{
"id": "deck",
"name": "deck",
"sample_id": null,
"children": [
"teaching_carrier"
],
"parent": "PLR_STATION",
"type": "deck",
"class": "OTDeck",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"type": "OTDeck",
"with_trash": false,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
}
},
"data": {}
},
{
"id": "teaching_carrier",
"name": "teaching_carrier",
"sample_id": null,
"children": [
"teaching_carrier_A1"
],
"parent": "deck",
"type": "plate",
"class": "opentrons_96_filtertiprack_1000ul",
"position": {
"x": 0,
"y": 0,
"z": 69
},
"config": {
"type": "Resource",
"size_x": 127,
"size_y": 85,
"size_z": 0,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": null,
"model": null
},
"data": {}
},
{
"id": "teaching_carrier_A1",
"name": "teaching_carrier_A1",
"sample_id": null,
"children": [],
"parent": "teaching_carrier",
"type": "device",
"class": "",
"position": {
"x": 10.87,
"y": 70.77,
"z": 9.47
},
"config": {
"type": "TipSpot",
"size_x": 6.86,
"size_y": 6.86,
"size_z": 10.67,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "tip_spot",
"model": null,
"prototype_tip": {
"type": "Tip",
"total_tip_length": 39.2,
"has_filter": true,
"maximal_volume": 20.0,
"fitting_depth": 3.29
}
},
"data": {
"liquids": [],
"pending_liquids": [],
"liquid_history": []
}
}
],
"links": [
]
}

View File

@@ -59,3 +59,5 @@ dependencies:
# ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo # ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo
# ilab equipments # ilab equipments
# - ros-humble-unilabos-msgs # - ros-humble-unilabos-msgs
- pip:
- paho-mqtt

View File

@@ -59,3 +59,5 @@ dependencies:
# ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo # ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo
# ilab equipments # ilab equipments
# - ros-humble-unilabos-msgs # - ros-humble-unilabos-msgs
- pip:
- paho-mqtt

View File

@@ -61,3 +61,5 @@ dependencies:
# ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo # ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo
# ilab equipments # ilab equipments
# - ros-humble-unilabos-msgs # - ros-humble-unilabos-msgs
- pip:
- paho-mqtt

View File

@@ -58,4 +58,6 @@ dependencies:
- ros-humble-simulation # ignored because of NO python3.11 package in WIN64 - ros-humble-simulation # ignored because of NO python3.11 package in WIN64
# ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo # ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo
# ilab equipments # ilab equipments
# - ros-humble-unilabos-msgs # ros-humble-unilabos-msgs
- pip:
- paho-mqtt

View File

@@ -7,11 +7,13 @@ from unilabos.utils import logger
def start_backend( def start_backend(
backend: str, backend: str,
devices_config: dict = {}, devices_config: dict = {},
resources_config: dict = {}, resources_config: list = [],
graph=None, graph=None,
controllers_config: dict = {}, controllers_config: dict = {},
bridges=[], bridges=[],
without_host: bool = False, without_host: bool = False,
visual: str = "None",
resources_mesh_config: dict = {},
**kwargs **kwargs
): ):
if backend == "ros": if backend == "ros":
@@ -29,7 +31,9 @@ def start_backend(
backend_thread = threading.Thread( backend_thread = threading.Thread(
target=main if not without_host else slave, target=main if not without_host else slave,
args=(devices_config, resources_config, graph, controllers_config, bridges) args=(devices_config, resources_config, graph, controllers_config, bridges, visual, resources_mesh_config),
name="backend_thread",
daemon=True,
) )
backend_thread.start() backend_thread.start()
logger.info(f"Backend {backend} started.") logger.info(f"Backend {backend} started.")

View File

@@ -29,6 +29,8 @@ def job_add(req: JobAddReq) -> JobData:
req.data['action'] = action_name req.data['action'] = action_name
if action_name == "execute_command_from_outer": if action_name == "execute_command_from_outer":
action_kwargs = {"command": json.dumps(action_kwargs)} action_kwargs = {"command": json.dumps(action_kwargs)}
elif "command" in action_kwargs:
action_kwargs = action_kwargs["command"]
print(f"job_add:{req.device_id} {action_name} {action_kwargs}") print(f"job_add:{req.device_id} {action_name} {action_kwargs}")
HostNode.get_instance().send_goal(req.device_id, action_name=action_name, action_kwargs=action_kwargs, goal_uuid=req.job_id) HostNode.get_instance().send_goal(req.device_id, action_name=action_name, action_kwargs=action_kwargs, goal_uuid=req.job_id)
return JobData(jobId=req.job_id) return JobData(jobId=req.job_id)

View File

@@ -1,19 +1,24 @@
import argparse import argparse
import asyncio
import json
import os import os
import signal import signal
import sys import sys
import json import threading
import yaml import time
from copy import deepcopy from copy import deepcopy
import yaml
# 首先添加项目根目录到路径 # 首先添加项目根目录到路径
current_dir = os.path.dirname(os.path.abspath(__file__)) current_dir = os.path.dirname(os.path.abspath(__file__))
ilabos_dir = os.path.dirname(os.path.dirname(current_dir)) unilabos_dir = os.path.dirname(os.path.dirname(current_dir))
if ilabos_dir not in sys.path: if unilabos_dir not in sys.path:
sys.path.append(ilabos_dir) sys.path.append(unilabos_dir)
from unilabos.config.config import load_config, BasicConfig, _update_config_from_env from unilabos.config.config import load_config, BasicConfig, _update_config_from_env
from unilabos.utils.banner_print import print_status, print_unilab_banner from unilabos.utils.banner_print import print_status, print_unilab_banner
from unilabos.device_mesh.resource_visalization import ResourceVisualization
def parse_args(): def parse_args():
@@ -65,12 +70,21 @@ def parse_args():
help="信息页web服务的启动端口", help="信息页web服务的启动端口",
) )
parser.add_argument( parser.add_argument(
"--open_browser", "--disable_browser",
type=bool, action='store_true',
default=True, help="是否在启动时关闭信息页",
help="是否在启动时打开信息页", )
parser.add_argument(
"--2d_vis",
action='store_true',
help="是否在pylabrobot实例启动时同时启动可视化",
)
parser.add_argument(
"--visual",
choices=["rviz", "web", "disable"],
default="disable",
help="选择可视化工具: rviz, web",
) )
return parser.parse_args() return parser.parse_args()
@@ -101,6 +115,7 @@ def main():
machine_name = os.popen("hostname").read().strip() machine_name = os.popen("hostname").read().strip()
machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name]) machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name])
BasicConfig.machine_name = machine_name BasicConfig.machine_name = machine_name
BasicConfig.vis_2d_enable = args_dict["2d_vis"]
from unilabos.resources.graphio import ( from unilabos.resources.graphio import (
read_node_link_json, read_node_link_json,
@@ -121,6 +136,7 @@ def main():
# 注册表 # 注册表
build_registry(args_dict["registry_path"]) build_registry(args_dict["registry_path"])
devices_and_resources = None
if args_dict["graph"] is not None: if args_dict["graph"] is not None:
import unilabos.resources.graphio as graph_res import unilabos.resources.graphio as graph_res
graph_res.physical_setup_graph = ( graph_res.physical_setup_graph = (
@@ -132,6 +148,7 @@ def main():
args_dict["resources_config"] = initialize_resources(list(deepcopy(devices_and_resources).values())) args_dict["resources_config"] = initialize_resources(list(deepcopy(devices_and_resources).values()))
args_dict["devices_config"] = dict_to_nested_dict(deepcopy(devices_and_resources), devices_only=False) args_dict["devices_config"] = dict_to_nested_dict(deepcopy(devices_and_resources), devices_only=False)
# args_dict["resources_config"] = dict_to_tree(devices_and_resources, devices_only=False) # args_dict["resources_config"] = dict_to_tree(devices_and_resources, devices_only=False)
args_dict["graph"] = graph_res.physical_setup_graph args_dict["graph"] = graph_res.physical_setup_graph
else: else:
if args_dict["devices"] is None or args_dict["resources"] is None: if args_dict["devices"] is None or args_dict["resources"] is None:
@@ -166,9 +183,28 @@ def main():
signal.signal(signal.SIGINT, _exit) signal.signal(signal.SIGINT, _exit)
signal.signal(signal.SIGTERM, _exit) signal.signal(signal.SIGTERM, _exit)
mqtt_client.start() mqtt_client.start()
args_dict["resources_mesh_config"] = {}
start_backend(**args_dict) # web visiualize 2D
start_server(port=args_dict.get("port", 8002), open_browser=args_dict.get("open_browser", False)) if args_dict["visual"] != "disable":
enable_rviz = args_dict["visual"] == "rviz"
if devices_and_resources is not None:
resource_visualization = ResourceVisualization(devices_and_resources, args_dict["resources_config"] ,enable_rviz=enable_rviz)
args_dict["resources_mesh_config"] = resource_visualization.resource_model
start_backend(**args_dict)
server_thread = threading.Thread(target=start_server, kwargs=dict(
open_browser=not args_dict["disable_browser"]
))
server_thread.start()
asyncio.set_event_loop(asyncio.new_event_loop())
resource_visualization.start()
while True:
time.sleep(1)
else:
start_backend(**args_dict)
start_server(open_browser=not args_dict["disable_browser"])
else:
start_backend(**args_dict)
start_server(open_browser=not args_dict["disable_browser"])
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -1,5 +1,6 @@
import json import json
import time import time
import traceback
import uuid import uuid
import paho.mqtt.client as mqtt import paho.mqtt.client as mqtt
@@ -35,7 +36,8 @@ class MQTTClient:
self.client.on_disconnect = self._on_disconnect self.client.on_disconnect = self._on_disconnect
def _on_log(self, client, userdata, level, buf): def _on_log(self, client, userdata, level, buf):
logger.info(f"[MQTT] log: {buf}") # logger.info(f"[MQTT] log: {buf}")
pass
def _on_connect(self, client, userdata, flags, rc, properties=None): def _on_connect(self, client, userdata, flags, rc, properties=None):
logger.info("[MQTT] Connected with result code " + str(rc)) logger.info("[MQTT] Connected with result code " + str(rc))
@@ -54,6 +56,12 @@ class MQTTClient:
logger.debug("Payload:", json.dumps(payload_json, indent=2, ensure_ascii=False)) logger.debug("Payload:", json.dumps(payload_json, indent=2, ensure_ascii=False))
if msg.topic == f"labs/{MQConfig.lab_id}/job/start/": if msg.topic == f"labs/{MQConfig.lab_id}/job/start/":
logger.debug("job_add", type(payload_json), payload_json) logger.debug("job_add", type(payload_json), payload_json)
if "data" not in payload_json:
payload_json["data"] = {}
if "action" in payload_json:
payload_json["data"]["action"] = payload_json.pop("action")
if "action_kwargs" in payload_json:
payload_json["data"]["action_kwargs"] = payload_json.pop("action_kwargs")
job_req = JobAddReq.model_validate(payload_json) job_req = JobAddReq.model_validate(payload_json)
data = job_add(job_req) data = job_add(job_req)
return return
@@ -61,8 +69,10 @@ class MQTTClient:
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
logger.error(f"[MQTT] JSON 解析错误: {e}") logger.error(f"[MQTT] JSON 解析错误: {e}")
logger.error(f"[MQTT] Raw message: {msg.payload}") logger.error(f"[MQTT] Raw message: {msg.payload}")
logger.error(traceback.format_exc())
except Exception as e: except Exception as e:
logger.error(f"[MQTT] 处理消息时出错: {e}") logger.error(f"[MQTT] 处理消息时出错: {e}")
logger.error(traceback.format_exc())
def _on_disconnect(self, client, userdata, rc, reasonCode=None, properties=None): def _on_disconnect(self, client, userdata, rc, reasonCode=None, properties=None):
if rc != 0: if rc != 0:

View File

@@ -9,6 +9,7 @@ from typing import List, Dict, Any, Optional
import requests import requests
from unilabos.utils.log import info from unilabos.utils.log import info
from unilabos.config.config import MQConfig, HTTPConfig from unilabos.config.config import MQConfig, HTTPConfig
from unilabos.utils import logger
class HTTPClient: class HTTPClient:
@@ -102,6 +103,30 @@ class HTTPClient:
) )
return response return response
def upload_file(self, file_path: str, scene: str = "models") -> requests.Response:
"""
上传文件到服务器
使用multipart/form-data格式上传文件类似curl -F "files=@filepath"
Args:
file_path: 要上传的文件路径
scene: 上传场景,可选值为"user""models",默认为"models"
Returns:
Response: API响应对象
"""
with open(file_path, "rb") as file:
files = {"files": file}
logger.info(f"上传文件: {file_path}{scene}")
response = requests.post(
f"{self.remote_addr}/api/account/file_upload/{scene}",
files=files,
headers={"Authorization": f"lab {self.auth}"},
timeout=30, # 上传文件可能需要更长的超时时间
)
return response
# 创建默认客户端实例 # 创建默认客户端实例
http_client = HTTPClient() http_client = HTTPClient()

View File

@@ -8,7 +8,7 @@ import traceback
from typing import Dict, Any, Type, TypedDict, Optional from typing import Dict, Any, Type, TypedDict, Optional
from rclpy.action import ActionClient, ActionServer from rclpy.action import ActionClient, ActionServer
from rosidl_parser.definition import UnboundedSequence, NamespacedType, BasicType from rosidl_parser.definition import UnboundedSequence, NamespacedType, BasicType, UnboundedString
from unilabos.ros.msgs.message_converter import msg_converter_manager from unilabos.ros.msgs.message_converter import msg_converter_manager
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
@@ -74,7 +74,6 @@ def get_yaml_from_goal_type(goal_type) -> str:
for ind, slot_info in enumerate(goal_type._fields_and_field_types.items()): for ind, slot_info in enumerate(goal_type._fields_and_field_types.items()):
slot_name, slot_type = slot_info slot_name, slot_type = slot_info
type_info = goal_type.SLOT_TYPES[ind] type_info = goal_type.SLOT_TYPES[ind]
default_value = "unknown"
if isinstance(type_info, UnboundedSequence): if isinstance(type_info, UnboundedSequence):
inner_type = type_info.value_type inner_type = type_info.value_type
if isinstance(inner_type, NamespacedType): if isinstance(inner_type, NamespacedType):
@@ -83,8 +82,10 @@ def get_yaml_from_goal_type(goal_type) -> str:
default_value = [get_ros_msg_instance_as_dict(type_class())] default_value = [get_ros_msg_instance_as_dict(type_class())]
elif isinstance(inner_type, BasicType): elif isinstance(inner_type, BasicType):
default_value = [get_default_value_for_ros_type(inner_type.typename)] default_value = [get_default_value_for_ros_type(inner_type.typename)]
elif isinstance(inner_type, UnboundedString):
default_value = [""]
else: else:
default_value = "unknown" default_value = []
elif isinstance(type_info, NamespacedType): elif 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)
@@ -93,6 +94,8 @@ def get_yaml_from_goal_type(goal_type) -> str:
default_value = get_ros_msg_instance_as_dict(type_class()) default_value = get_ros_msg_instance_as_dict(type_class())
elif isinstance(type_info, BasicType): elif isinstance(type_info, BasicType):
default_value = get_default_value_for_ros_type(type_info.typename) default_value = get_default_value_for_ros_type(type_info.typename)
elif isinstance(type_info, UnboundedString):
default_value = ""
else: else:
type_class = msg_converter_manager.search_class(slot_type, search_lower=True) type_class = msg_converter_manager.search_class(slot_type, search_lower=True)
if type_class is not None: if type_class is not None:

View File

@@ -13,6 +13,7 @@ class BasicConfig:
is_host_mode = True # 从registry.py移动过来 is_host_mode = True # 从registry.py移动过来
slave_no_host = False # 是否跳过rclient.wait_for_service() slave_no_host = False # 是否跳过rclient.wait_for_service()
machine_name = "undefined" machine_name = "undefined"
vis_2d_enable = False
# MQTT配置 # MQTT配置

View File

View File

@@ -0,0 +1,18 @@
{
"first_joint": {
"child":"first_link",
"axis" : "-y"
},
"second_joint": {
"child":"second_link",
"axis" : "-x"
},
"third_joint": {
"child":"third_link",
"axis" : "-z"
},
"fourth_joint": {
"child":"fourth_link",
"axis" : "-z"
}
}

View File

@@ -0,0 +1,210 @@
<?xml version="1.0" ?>
<robot xmlns:xacro="http://ros.org/wiki/xacro">
<xacro:macro name="opentrons_liquid_handler"
params="parent_link:='' station_name:='' device_name:='' x:=0 y:=0 z:=0 r:=0 mesh_path:=''">
<joint name="${station_name}${device_name}base_link_joint" type="fixed">
<origin xyz="${x} ${y} ${z}" rpy="0 0 ${r}" />
<parent link="${parent_link}"/>
<child link="${station_name}${device_name}device_link"/>
<axis xyz="0 0 0"/>
</joint>
<link name="${station_name}${device_name}device_link"/>
<joint name="${station_name}${device_name}device_link_joint" type="fixed">
<origin xyz="-0.11565 0.496 0" rpy="0 0 0" />
<parent link="${station_name}${device_name}device_link"/>
<child link="${station_name}${device_name}main_link"/>
<axis xyz="0 0 0"/>
</joint>
<link name='${station_name}${device_name}main_link'>
<visual>
<geometry>
<mesh filename="file://${mesh_path}/devices/opentrons_liquid_handler/meshes/ot2-0.stl"/>
</geometry>
<material name="">
<color rgba="0.756862745098039 0.768627450980392 0.752941176470588 1"/>
</material>
</visual>
<collision>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/opentrons_liquid_handler/meshes/ot2-0.stl"/>
</geometry>
</collision>
</link>
<link name='${station_name}${device_name}first_link'>
<visual>
<geometry>
<mesh filename="file://${mesh_path}/devices/opentrons_liquid_handler/meshes/ot2-1.stl"/>
</geometry>
<material name="">
<color rgba="0.756862745098039 0.768627450980392 0.752941176470588 1"/>
</material>
</visual>
<collision>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/opentrons_liquid_handler/meshes/ot2-1.stl"/>
</geometry>
</collision>
</link>
<link name='${station_name}${device_name}second_link'>
<visual>
<geometry>
<mesh filename="file://${mesh_path}/devices/opentrons_liquid_handler/meshes/ot2-2.stl"/>
</geometry>
<material name="">
<color rgba="0.756862745098039 0.768627450980392 0.752941176470588 1"/>
</material>
</visual>
<collision>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/opentrons_liquid_handler/meshes/ot2-2.stl"/>
</geometry>
</collision>
</link>
<link name='${station_name}${device_name}third_link'>
<visual>
<geometry>
<mesh filename="file://${mesh_path}/devices/opentrons_liquid_handler/meshes/ot2-3a.stl"/>
</geometry>
<material name="">
<color rgba="0.756862745098039 0.768627450980392 0.752941176470588 1"/>
</material>
</visual>
<collision>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/opentrons_liquid_handler/meshes/ot2-3a.stl"/>
</geometry>
</collision>
</link>
<link name='${station_name}${device_name}fourth_link'>
<visual>
<geometry>
<mesh filename="file://${mesh_path}/devices/opentrons_liquid_handler/meshes/ot2-3b.stl"/>
</geometry>
<material name="">
<color rgba="0.756862745098039 0.768627450980392 0.752941176470588 1"/>
</material>
</visual>
<collision>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/opentrons_liquid_handler/meshes/ot2-3b.stl"/>
</geometry>
</collision>
</link>
<joint name='${station_name}${device_name}first_joint' type='prismatic'>
<parent link="${station_name}${device_name}main_link"/>
<child link="${station_name}${device_name}first_link"/>
<origin rpy="0 0 0" xyz="0 0 0"/>
<axis xyz="0 -1 0"/>
<limit effort="-1" lower="-0.2" upper="0.13" velocity="-1"/>
<dynamics damping="0.1"/>
</joint>
<joint name='${station_name}${device_name}second_joint' type='prismatic'>
<parent link="${station_name}${device_name}first_link"/>
<child link="${station_name}${device_name}second_link"/>
<origin rpy="0 0 0" xyz="0 0 0"/>
<axis xyz="-1 0 0"/>
<limit effort="-1" lower="-0.15" upper="0.15" velocity="-1"/>
<dynamics damping="0.1"/>
</joint>
<joint name='${station_name}${device_name}third_joint' type='prismatic'>
<parent link="${station_name}${device_name}second_link"/>
<child link="${station_name}${device_name}third_link"/>
<origin rpy="0 0 0" xyz="0 0 0"/>
<axis xyz="0 0 -1"/>
<limit effort="-1" lower="0" upper="0.22" velocity="-1"/>
<dynamics damping="0.1"/>
</joint>
<joint name='${station_name}${device_name}fourth_joint' type='prismatic'>
<parent link="${station_name}${device_name}second_link"/>
<child link="${station_name}${device_name}fourth_link"/>
<origin rpy="0 0 0" xyz="0 0 0"/>
<axis xyz="0 0 -1"/>
<limit effort="-1" lower="0" upper="0.22" velocity="-1"/>
<dynamics damping="0.1"/>
</joint>
<link name='${station_name}${device_name}socketTypeGenericSbsFootprint'/>
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_10_60_1' type='fixed'>
<parent link="${station_name}${device_name}main_link"/>
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
<origin rpy="0 0 -1.57" xyz="0.1795 -0.1825 0.07"/>
</joint>
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_7_60_1' type='fixed'>
<parent link="${station_name}${device_name}main_link"/>
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
<origin rpy="0 0 -1.57" xyz="0.1795 -0.273 0.07"/>
</joint>
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_4_60_1' type='fixed'>
<parent link="${station_name}${device_name}main_link"/>
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
<origin rpy="0 0 -1.57" xyz="0.1795 -0.3635 0.07"/>
</joint>
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_1_60_1' type='fixed'>
<parent link="${station_name}${device_name}main_link"/>
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
<origin rpy="0 0 -1.57" xyz="0.1795 -0.454 0.07"/>
</joint>
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_11_60_1' type='fixed'>
<parent link="${station_name}${device_name}main_link"/>
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
<origin rpy="0 0 -1.57" xyz="0.312 -0.1825 0.07"/>
</joint>
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_8_60_1' type='fixed'>
<parent link="${station_name}${device_name}main_link"/>
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
<origin rpy="0 0 -1.57" xyz="0.312 -0.273 0.07"/>
</joint>
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_5_60_1' type='fixed'>
<parent link="${station_name}${device_name}main_link"/>
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
<origin rpy="0 0 -1.57" xyz="0.312 -0.3635 0.07"/>
</joint>
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_2_60_1' type='fixed'>
<parent link="${station_name}${device_name}main_link"/>
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
<origin rpy="0 0 -1.57" xyz="0.312 -0.454 0.07"/>
</joint>
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_9_60_1' type='fixed'>
<parent link="${station_name}${device_name}main_link"/>
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
<origin rpy="0 0 -1.57" xyz="0.4445 -0.273 0.07"/>
</joint>
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_6_60_1' type='fixed'>
<parent link="${station_name}${device_name}main_link"/>
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
<origin rpy="0 0 -1.57" xyz="0.4445 -0.3635 0.07"/>
</joint>
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_3_60_1' type='fixed'>
<parent link="${station_name}${device_name}main_link"/>
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
<origin rpy="0 0 -1.57" xyz="0.4445 -0.454 0.07"/>
</joint>
<link name='${station_name}${device_name}socketTypeHEPAModule'/>
<joint name='${station_name}${device_name}socketTypeHEPAModule' type='fixed'>
<parent link="${station_name}${device_name}main_link"/>
<child link="${station_name}${device_name}socketTypeHEPAModule"/>
<origin rpy="0 0 0" xyz="0.31 -0.26 0.66"/>
</joint>
</xacro:macro>
</robot>

View File

@@ -0,0 +1,10 @@
{
"private_param":
{
},
"public_param":
{
}
}

View File

@@ -0,0 +1,6 @@
{
"slider_joint": {
"child":"slider",
"axis" : "x"
}
}

View File

@@ -0,0 +1,136 @@
<?xml version="1.0" ?>
<!-- =================================================================================== -->
<!-- | This document was autogenerated by xacro from slide.urdf | -->
<!-- | EDITING THIS FILE BY HAND IS NOT RECOMMENDED | -->
<!-- =================================================================================== -->
<!-- This URDF was automatically created by SolidWorks to URDF Exporter! Originally created by Stephen Brawner (brawner@gmail.com)
Commit Version: 1.6.0-4-g7f85cfe Build Version: 1.6.7995.38578
For more information, please see http://wiki.ros.org/sw_urdf_exporter -->
<robot xmlns:xacro="http://ros.org/wiki/xacro">
<xacro:macro name="slide_w140" params="mesh_path:='' length:=0.1 min_d:=0.1 max_d:=0.1 slider_d:=0.14 parent_link:='' station_name:='' device_name:='' x:=0 y:=0 z:=0 r:=0" >
<joint name="${station_name}${device_name}base_link_joint" type="fixed">
<origin xyz="${x} ${y} ${z}" rpy="0 0 ${r}" />
<parent link="${parent_link}"/>
<child link="${station_name}${device_name}device_link"/>
<axis xyz="0 0 0"/>
</joint>
<link name="${station_name}${device_name}device_link"/>
<joint name="${station_name}${device_name}device_link_joint" type="fixed">
<origin xyz="${-min_d} 0 0" rpy="0 0 0" />
<parent link="${station_name}${device_name}device_link"/>
<child link="${station_name}${device_name}base_link"/>
<axis xyz="0 0 0"/>
</joint>
<link name="${station_name}${device_name}base_link">
<inertial>
<origin rpy="0 0 0" xyz="-0.00073944 -0.070732 0.035966"/>
<mass value="0.88697"/>
<inertia ixx="0.0019162" ixy="-2.5282E-11" ixz="6.1083E-07" iyy="0.00066605" iyz="1.4627E-08" izz="0.0019573"/>
</inertial>
<visual>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/slide_w140/meshes/base_link.STL"/>
</geometry>
<material name="">
<color rgba="1 1 1 1"/>
</material>
</visual>
<collision>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/slide_w140/meshes/base_link.STL"/>
</geometry>
</collision>
</link>
<link name="${station_name}${device_name}slider">
<inertial>
<origin rpy="0 0 0" xyz="0 -4.7184E-16 0.0095496"/>
<mass value="0.27913"/>
<inertia ixx="0.00048249" ixy="2.0866E-19" ixz="2.788E-21" iyy="0.00047014" iyz="1.1542E-17" izz="0.0009242"/>
</inertial>
<visual>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/slide_w140/meshes/slider.STL" />
</geometry>
<material name="">
<color rgba="1 1 1 1"/>
</material>
</visual>
<collision>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/slide_w140/meshes/slider.STL" />
</geometry>
</collision>
</link>
<joint name="${station_name}${device_name}slider_joint" type="prismatic">
<origin rpy="0 0 0" xyz="${min_d + slider_d/2} 0 0.0475"/>
<parent link="${station_name}${device_name}base_link"/>
<child link="${station_name}${device_name}slider"/>
<axis xyz="1 0 0"/>
<limit effort="100" lower="0" upper="${length}" velocity="1"/>
</joint>
<link name="${station_name}${device_name}length">
<inertial>
<origin rpy="0 0 0" xyz="0.005 -9.3044E-10 0.02803"/>
<mass value="0.050532"/>
<inertia ixx="8.098E-05" ixy="-1.2574E-19" ixz="3.1207E-20" iyy="4.4152E-06" iyz="-3.1294E-13" izz="7.7407E-05"/>
</inertial>
<visual>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/slide_w140/meshes/length.STL" scale="${(length + min_d + max_d + slider_d)} 1 1"/>
</geometry>
<material name="">
<color rgba="1 1 1 1"/>
</material>
</visual>
<collision>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/slide_w140/meshes/length.STL" scale="${(length + min_d + max_d + slider_d)} 1 1"/>
</geometry>
</collision>
</link>
<joint name="${station_name}${device_name}length_joint" type="fixed">
<origin rpy="0 0 0" xyz="0 0 0"/>
<parent link="${station_name}${device_name}base_link"/>
<child link="${station_name}${device_name}length"/>
</joint>
<link name="${station_name}${device_name}slide_end">
<inertial>
<origin rpy="0 0 0" xyz="0.003 8.6044E-06 0.035593"/>
<mass value="0.055452"/>
<inertia ixx="9.9729E-05" ixy="-1.0228E-21" ixz="3.8344E-22" iyy="2.3158E-05" iyz="1.5613E-08" izz="7.6904E-05"/>
</inertial>
<visual>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/slide_w140/meshes/slide_end.STL"/>
</geometry>
<material name="">
<color rgba="1 1 1 1"/>
</material>
</visual>
<collision>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/slide_w140/meshes/slide_end.STL"/>
</geometry>
</collision>
</link>
<joint name="${station_name}${device_name}slide_end_joint" type="fixed">
<origin rpy="0 0 0" xyz="${length + max_d +min_d + slider_d} 0 0"/>
<parent link="${station_name}${device_name}base_link"/>
<child link="${station_name}${device_name}slide_end"/>
</joint>
</xacro:macro>
</robot>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,12 @@
{
"private_param":
{
"min_d": 0.1 ,
"max_d": 0.1 ,
"slider_d": 0.14
},
"public_param":
{
"length" :0.1
}
}

View File

@@ -0,0 +1,174 @@
import os
from pathlib import Path
from launch import LaunchService
from launch import LaunchDescription
from launch_ros.actions import Node as nd
import xacro
from lxml import etree
from unilabos.registry.registry import lab_registry
class ResourceVisualization:
def __init__(self, device: dict, resource: dict, enable_rviz: bool = True):
"""初始化资源可视化类
该类用于将设备和资源的3D模型可视化展示。通过解析设备和资源的配置信息,
从注册表中获取对应的3D模型文件,并使用ROS2和RViz进行可视化。
Args:
device (dict): 设备配置字典,包含设备的类型、位置等信息
resource (dict): 资源配置字典,包含资源的类型、位置等信息
registry (dict): 注册表字典,包含设备和资源类型的注册信息
enable_rviz (bool, optional): 是否启用RViz可视化. Defaults to True.
"""
self.launch_service = LaunchService()
self.launch_description = LaunchDescription()
self.resource_dict = resource
self.resource_model = {}
self.resource_type = ['deck', 'plate', 'container']
self.mesh_path = Path(__file__).parent.absolute()
self.enable_rviz = enable_rviz
registry = lab_registry
self.srdf_str = '''
<?xml version="1.0" encoding="UTF-8"?>
<robot name="minimal">
</robot>
'''
self.robot_state_str= '''<?xml version="1.0" ?>
<robot xmlns:xacro="http://ros.org/wiki/xacro" name="full_dev">
<link name="world"/>
</robot>
'''
self.root = etree.fromstring(self.robot_state_str)
xacro_uri = self.root.nsmap["xacro"]
# 遍历设备节点
for node in device.values():
if node['type'] == 'device' and node['class'] != '':
device_class = node['class']
# 检查设备类型是否在注册表中
if device_class not in registry.device_type_registry.keys():
raise ValueError(f"设备类型 {device_class} 未在注册表中注册")
elif node['type'] in self.resource_type:
# print(registry.resource_type_registry)
resource_class = node['class']
if resource_class not in registry.resource_type_registry.keys():
raise ValueError(f"资源类型 {resource_class} 未在注册表中注册")
elif "model" in registry.resource_type_registry[resource_class].keys():
model_config = registry.resource_type_registry[resource_class]['model']
if model_config['type'] == 'resource':
self.resource_model[node['id']] = {
'mesh': f"{str(self.mesh_path)}/resources/{model_config['mesh']}",
'mesh_tf': model_config['mesh_tf']}
if 'children_mesh' in model_config:
if model_config['children_mesh'] is not None:
self.resource_model[f"{node['id']}_"] = {
'mesh': f"{str(self.mesh_path)}/resources/{model_config['children_mesh']}",
'mesh_tf': model_config['children_mesh_tf']
}
elif model_config['type'] == 'device':
new_include = etree.SubElement(self.root, f"{{{xacro_uri}}}include")
new_include.set("filename", f"{str(self.mesh_path)}/devices/{model_config['mesh']}/macro_device.xacro")
new_dev = etree.SubElement(self.root, f"{{{xacro_uri}}}{model_config['mesh']}")
new_dev.set("parent_link", "world")
new_dev.set("mesh_path", str(self.mesh_path))
new_dev.set("device_name", node["id"]+"_")
new_dev.set("station_name", node["parent"]+'_')
new_dev.set("x",str(float(node["position"]["x"])/1000))
new_dev.set("y",str(float(node["position"]["y"])/1000))
new_dev.set("z",str(float(node["position"]["z"])/1000))
if "rotation" in node["config"]:
new_dev.set("r",str(float(node["config"]["rotation"]["z"])/1000))
else:
print("错误的注册表类型!")
re = etree.tostring(self.root, encoding="unicode")
doc = xacro.parse(re)
xacro.process_doc(doc)
self.urdf_str = doc.toxml()
def create_launch_description(self, urdf_str: str) -> LaunchDescription:
"""
创建launch描述包含robot_state_publisher和move_group节点
Args:
urdf_str: URDF文本
Returns:
LaunchDescription: launch描述对象
"""
# 解析URDF文件
robot_description = urdf_str
# 创建robot_state_publisher节点
robot_state_publisher = nd(
package='robot_state_publisher',
executable='robot_state_publisher',
name='robot_state_publisher',
output='screen',
parameters=[{
'robot_description': robot_description,
'use_sim_time': False
}]
)
# joint_state_publisher_node = nd(
# package='joint_state_publisher_gui', # 或 joint_state_publisher
# executable='joint_state_publisher_gui',
# name='joint_state_publisher',
# output='screen'
# )
# 创建move_group节点
move_group = nd(
package='moveit_ros_move_group',
executable='move_group',
output='screen',
parameters=[{
'robot_description': robot_description,
'robot_description_semantic': self.srdf_str,
'capabilities': '',
'disable_capabilities': '',
'monitor_dynamics': False,
'publish_monitored_planning_scene': True,
'publish_robot_description_semantic': True,
'publish_planning_scene': True,
'publish_geometry_updates': True,
'publish_state_updates': True,
'publish_transforms_updates': True,
}]
)
# 将节点添加到launch描述中
self.launch_description.add_action(robot_state_publisher)
# self.launch_description.add_action(joint_state_publisher_node)
self.launch_description.add_action(move_group)
# 如果启用RViz,添加RViz节点
if self.enable_rviz:
rviz_node = nd(
package='rviz2',
executable='rviz2',
name='rviz2',
arguments=['-d', f"{str(self.mesh_path)}/view_robot.rviz"],
output='screen'
)
self.launch_description.add_action(rviz_node)
return self.launch_description
def start(self) -> None:
"""
启动可视化服务
Args:
urdf_str: URDF文件路径
"""
launch_description = self.create_launch_description(self.urdf_str)
self.launch_service.include_launch_description(launch_description)
self.launch_service.run()

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@@ -0,0 +1,22 @@
{
"fileName": "generic_labware_tube_10_75",
"related": [
"generic_labware_0.5ml_screw_cap_tube",
"generic_labware_0.5ml_tube_rack",
"generic_labware_12_well_plate",
"sarstedt_14x200mm_tube",
"sarstedt_18x200mm_tube",
"generic_labware_1ml_tube_rack",
"generic_labware_24_well_plate",
"generic_labware_2ml_screw_cap_tube",
"generic_labware_5ml_screw_cap_tube",
"generic_labware_6_well_plate",
"generic_labware_96_well_square",
"generic_labware_96_well_pcr_plate_round",
"generic_labware_framedtiprack",
"generic_labware_plate_lid",
"generic_labware_reservoir",
"generic_labware_tip_box",
"generic_labware_tube_10_75"
]
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" ?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name="generic_labware_tube_10_75">
<link name='0_base_link'>
<visual name='visual'>
<geometry>
<mesh filename="meshes/0_base.stl"/>
</geometry>
<material name="clay" />
</visual>
</link>
</robot>

View File

@@ -0,0 +1,86 @@
{
"fileName": "tecan_nested_tip_rack",
"related": [
"tecan_techrom",
"tecan_holder_transfer_tool",
"tecan_fluent_9_grid_segment_cutout",
"tecan_fluent_centric_gripper",
"tecan_fluent_eccentric_gripper",
"tecan_evo100",
"tecan_fluent_mp_diti_nest_segment",
"tecan_fluent_4x100_trough",
"tecan_fluent_1080_extended",
"tecan_fluent_1_1_1000_trough",
"tecan_fluent_50ml_tube_runner_10_v2",
"tecan_fluent_15ml_tube_runner_16_v2",
"tecan_fluent_1_16_16_tube_runner",
"tecan_fluent_1.5ml_tube_runner_v2",
"tecan_fluent_1_24_10_tube_runner",
"tecan_fluent_1_24_13_tube_runner",
"tecan_fluent_3x320_reagent_trough_v2",
"tecan_fluent_32_tube_runner_v2",
"tecan_fluent_1_4_100_trough",
"tecan_fluent_2_grid_segment",
"tecan_fluent_2_4_100_trough_waste",
"tecan_fluent_3_grid_segment",
"tecan_fluent_nest_waste_segment_v2",
"tecan_fluent_320ml_reagent_trough",
"tecan_fluent_4_landscape_61mm_nest_segment",
"tecan_fluent_4_landscape_61mm_nest_segment_waste",
"tecan_fluent_4_landscape_7mm_nest_segment",
"tecan_fluent_4_landscape_7mm_nest_segment_waste",
"tecan_fluent_hotel_deck_4",
"tecan_fluent_480_extended",
"tecan_fluent_4x100_reagent_trough_v2",
"tecan_fluent_5_landscape_61mm_nest_segment",
"tecan_fluent_5_landscape_7mm_nest_segment",
"tecan_fluent_hotel_deck_5",
"tecan_fluent_6_grid_segment",
"tecan_fluent_nest_landscape_segment_v2",
"tecan_fluent_6_landscape_7mm_nest_segment",
"tecan_fluent_deck_segment_6_v2",
"tecan_fluent_fca_diti_segment_v2",
"tecan_fluent_6_nest_incubator",
"tecan_fluent_plate_nest",
"tecan_fluent_780_extended",
"tecan_fluent_plate_holder",
"tecan_fluent_8_grid_segment",
"tecan_fluent_8_grid_segment_evo",
"tecan_fluent_hotel_deck_9",
"tecan_carousel",
"tecan_carousel_stacker_10",
"tecan_carousel_stacker_25",
"tecan_carousel_stacker_6",
"tecan_fluent_coolheat_microplate_segment_v2",
"tecan_fluent_fca_diti_tray",
"tecan_fluent_trough_waste",
"tecan_fluent_id_left",
"tecan_fluent_id_middle",
"tecan_fluent_lower_6_grid_v2",
"tecan_fluent_mc384_nest",
"tecan_fluent_mca_44mm_nest",
"tecan_fluent_deck_segment_4_v2",
"tecan_fluent_mca_base_segment_384_v2",
"tecan_fluent_waste_module",
"tecan_fluent_reagent_block",
"tecan_fluent_tube_grippers",
"tecan_fluent_washstation_waste_v2",
"tecan_carrier_additive_trough_3_pce_max_100ml",
"tecan_carrier_384_well_mp_3_pos_accessible_roma",
"tecan_carrier_rack_3_diti_width_6",
"tecan_transport_box_diti_tray_1000ul",
"tecan_transport_box_diti_tray_200ul",
"tecan_magicprep_ngs_sample_deck",
"tecan_fluent_shelf_large",
"tecan_fluent_shelf_small",
"tecan_spacer_29_9_te_chrom",
"tecan_teshake_adapter_2",
"tecan_teshake_base",
"tecan_tevacs_base",
"tecan_tevacs_plate_park",
"tecan_tevacs_spacer",
"tecan_tevacs_vacuum",
"tecan_tip_box",
"tecan_nested_tip_rack"
]
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" ?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name="tecan_nested_tip_rack">
<link name='plate_link'>
<visual name='visual'>
<geometry>
<mesh filename="meshes/plate.stl"/>
</geometry>
<material name="clay" />
</visual>
</link>
</robot>

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

View File

@@ -0,0 +1,387 @@
Panels:
- Class: rviz_common/Displays
Help Height: 138
Name: Displays
Property Tree Widget:
Expanded:
- /Global Options1
- /TF1
- /TF1/Tree1
- /RobotModel1
- /PlanningScene1
- /PlanningScene1/Scene Geometry1
- /RobotState1
- /RobotState1/Links1
- /MotionPlanning1
- /MotionPlanning1/Scene Geometry1
- /MotionPlanning1/Scene Robot1
- /MotionPlanning1/Planning Request1
Splitter Ratio: 0.5
Tree Height: 345
- Class: rviz_common/Selection
Name: Selection
- Class: rviz_common/Tool Properties
Expanded:
- /2D Goal Pose1
- /Publish Point1
Name: Tool Properties
Splitter Ratio: 0.5886790156364441
- Class: rviz_common/Views
Expanded:
- /Current View1
Name: Views
Splitter Ratio: 0.5
Visualization Manager:
Class: ""
Displays:
- Alpha: 0.5
Cell Size: 1
Class: rviz_default_plugins/Grid
Color: 160; 160; 164
Enabled: true
Line Style:
Line Width: 0.029999999329447746
Value: Lines
Name: Grid
Normal Cell Count: 0
Offset:
X: 0
Y: 0
Z: 0
Plane: XY
Plane Cell Count: 10
Reference Frame: <Fixed Frame>
Value: true
- Class: rviz_default_plugins/TF
Enabled: false
Frame Timeout: 15
Frames:
All Enabled: false
Marker Scale: 1
Name: TF
Show Arrows: true
Show Axes: true
Show Names: false
Tree:
{}
Update Interval: 0
Value: false
- Alpha: 1
Class: rviz_default_plugins/RobotModel
Collision Enabled: false
Description File: ""
Description Source: Topic
Description Topic:
Depth: 5
Durability Policy: Volatile
History Policy: Keep Last
Reliability Policy: Reliable
Value: /robot_description
Enabled: false
Links:
All Links Enabled: true
Expand Joint Details: false
Expand Link Details: false
Expand Tree: false
Link Tree Style: Links in Alphabetic Order
Mass Properties:
Inertia: false
Mass: false
Name: RobotModel
TF Prefix: ""
Update Interval: 0
Value: false
Visual Enabled: true
- Class: moveit_rviz_plugin/PlanningScene
Enabled: false
Move Group Namespace: ""
Name: PlanningScene
Planning Scene Topic: /monitored_planning_scene
Robot Description: robot_description
Scene Geometry:
Scene Alpha: 0.8999999761581421
Scene Color: 50; 230; 50
Scene Display Time: 0.009999999776482582
Show Scene Geometry: true
Voxel Coloring: Z-Axis
Voxel Rendering: Occupied Voxels
Scene Robot:
Attached Body Color: 150; 50; 150
Links:
All Links Enabled: true
Expand Joint Details: false
Expand Link Details: false
Expand Tree: false
Link Tree Style: Links in Alphabetic Order
Robot Alpha: 1
Show Robot Collision: false
Show Robot Visual: false
Value: false
- Attached Body Color: 150; 50; 150
Class: moveit_rviz_plugin/RobotState
Collision Enabled: false
Enabled: false
Links:
All Links Enabled: true
Expand Joint Details: false
Expand Link Details: false
Expand Tree: false
Link Tree Style: Links in Alphabetic Order
Name: RobotState
Robot Alpha: 1
Robot Description: robot_description
Robot State Topic: display_robot_state
Show All Links: true
Show Highlights: true
Value: false
Visual Enabled: true
- Acceleration_Scaling_Factor: 0.1
Class: moveit_rviz_plugin/MotionPlanning
Enabled: true
Move Group Namespace: ""
MoveIt_Allow_Approximate_IK: false
MoveIt_Allow_External_Program: false
MoveIt_Allow_Replanning: false
MoveIt_Allow_Sensor_Positioning: false
MoveIt_Planning_Attempts: 10
MoveIt_Planning_Time: 5
MoveIt_Use_Cartesian_Path: false
MoveIt_Use_Constraint_Aware_IK: false
MoveIt_Workspace:
Center:
X: 0
Y: 0
Z: 0
Size:
X: 2
Y: 2
Z: 2
Name: MotionPlanning
Planned Path:
Color Enabled: false
Interrupt Display: false
Links:
All Links Enabled: true
Expand Joint Details: false
Expand Link Details: false
Expand Tree: false
Link Tree Style: Links in Alphabetic Order
PLR_STATION_deck_device_link:
Alpha: 1
Show Axes: false
Show Trail: false
PLR_STATION_deck_first_link:
Alpha: 1
Show Axes: false
Show Trail: false
Value: true
PLR_STATION_deck_fourth_link:
Alpha: 1
Show Axes: false
Show Trail: false
Value: true
PLR_STATION_deck_main_link:
Alpha: 1
Show Axes: false
Show Trail: false
Value: true
PLR_STATION_deck_second_link:
Alpha: 1
Show Axes: false
Show Trail: false
Value: true
PLR_STATION_deck_socketTypeGenericSbsFootprint:
Alpha: 1
Show Axes: false
Show Trail: false
PLR_STATION_deck_socketTypeHEPAModule:
Alpha: 1
Show Axes: false
Show Trail: false
PLR_STATION_deck_third_link:
Alpha: 1
Show Axes: false
Show Trail: false
Value: true
world:
Alpha: 1
Show Axes: false
Show Trail: false
Loop Animation: false
Robot Alpha: 0.5
Robot Color: 150; 50; 150
Show Robot Collision: false
Show Robot Visual: true
Show Trail: false
State Display Time: 3x
Trail Step Size: 1
Trajectory Topic: /display_planned_path
Use Sim Time: false
Planning Metrics:
Payload: 1
Show Joint Torques: false
Show Manipulability: false
Show Manipulability Index: false
Show Weight Limit: false
TextHeight: 0.07999999821186066
Planning Request:
Colliding Link Color: 255; 0; 0
Goal State Alpha: 1
Goal State Color: 250; 128; 0
Interactive Marker Size: 0
Joint Violation Color: 255; 0; 255
Planning Group: ""
Query Goal State: false
Query Start State: false
Show Workspace: false
Start State Alpha: 1
Start State Color: 0; 255; 0
Planning Scene Topic: /monitored_planning_scene
Robot Description: robot_description
Scene Geometry:
Scene Alpha: 0.8999999761581421
Scene Color: 50; 230; 50
Scene Display Time: 0.009999999776482582
Show Scene Geometry: true
Voxel Coloring: Z-Axis
Voxel Rendering: Occupied Voxels
Scene Robot:
Attached Body Color: 150; 50; 150
Links:
All Links Enabled: true
Expand Joint Details: false
Expand Link Details: false
Expand Tree: false
Link Tree Style: Links in Alphabetic Order
PLR_STATION_deck_device_link:
Alpha: 1
Show Axes: false
Show Trail: false
PLR_STATION_deck_first_link:
Alpha: 1
Show Axes: false
Show Trail: false
Value: true
PLR_STATION_deck_fourth_link:
Alpha: 1
Show Axes: false
Show Trail: false
Value: true
PLR_STATION_deck_main_link:
Alpha: 1
Show Axes: false
Show Trail: false
Value: true
PLR_STATION_deck_second_link:
Alpha: 1
Show Axes: false
Show Trail: false
Value: true
PLR_STATION_deck_socketTypeGenericSbsFootprint:
Alpha: 1
Show Axes: false
Show Trail: false
PLR_STATION_deck_socketTypeHEPAModule:
Alpha: 1
Show Axes: false
Show Trail: false
PLR_STATION_deck_third_link:
Alpha: 1
Show Axes: false
Show Trail: false
Value: true
world:
Alpha: 1
Show Axes: false
Show Trail: false
Robot Alpha: 1
Show Robot Collision: false
Show Robot Visual: true
Value: true
Velocity_Scaling_Factor: 0.1
Enabled: true
Global Options:
Background Color: 48; 48; 48
Fixed Frame: world
Frame Rate: 30
Name: root
Tools:
- Class: rviz_default_plugins/Interact
Hide Inactive Objects: true
- Class: rviz_default_plugins/MoveCamera
- Class: rviz_default_plugins/Select
- Class: rviz_default_plugins/FocusCamera
- Class: rviz_default_plugins/Measure
Line color: 128; 128; 0
- Class: rviz_default_plugins/SetInitialPose
Covariance x: 0.25
Covariance y: 0.25
Covariance yaw: 0.06853891909122467
Topic:
Depth: 5
Durability Policy: Volatile
History Policy: Keep Last
Reliability Policy: Reliable
Value: /initialpose
- Class: rviz_default_plugins/SetGoal
Topic:
Depth: 5
Durability Policy: Volatile
History Policy: Keep Last
Reliability Policy: Reliable
Value: /goal_pose
- Class: rviz_default_plugins/PublishPoint
Single click: true
Topic:
Depth: 5
Durability Policy: Volatile
History Policy: Keep Last
Reliability Policy: Reliable
Value: /clicked_point
Transformation:
Current:
Class: rviz_default_plugins/TF
Value: true
Views:
Current:
Class: rviz_default_plugins/Orbit
Distance: 1.0284695625305176
Enable Stereo Rendering:
Stereo Eye Separation: 0.05999999865889549
Stereo Focal Distance: 1
Swap Stereo Eyes: false
Value: false
Focal Point:
X: 0.29730814695358276
Y: 0.21228469908237457
Z: 0.20008830726146698
Focal Shape Fixed Size: true
Focal Shape Size: 0.05000000074505806
Invert Z Axis: false
Name: Current View
Near Clip Distance: 0.009999999776482582
Pitch: 0.38979560136795044
Target Frame: <Fixed Frame>
Value: Orbit (rviz)
Yaw: 0.06074193865060806
Saved: ~
Window Geometry:
Displays:
collapsed: false
Height: 1656
Hide Left Dock: false
Hide Right Dock: true
MotionPlanning:
collapsed: false
MotionPlanning - Trajectory Slider:
collapsed: false
QMainWindow State: 000000ff00000000fd0000000400000000000003a3000005dcfc020000000bfb0000001200530065006c0065006300740069006f006e00000001e10000009b000000b000fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c006100790073010000006e000002510000018200fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261fb000000280020002d0020005400720061006a006500630074006f0072007900200053006c00690064006500720000000000ffffffff0000000000000000fb00000044004d006f00740069006f006e0050006c0061006e006e0069006e00670020002d0020005400720061006a006500630074006f0072007900200053006c00690064006500720000000000ffffffff0000007a00fffffffb0000001c004d006f00740069006f006e0050006c0061006e006e0069006e006701000002cb0000037f000002b800ffffff000000010000010f00000387fc0200000003fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a00560069006500770073000000003b000003870000013200fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e10000019700000003000004420000003efc0100000002fb0000000800540069006d00650100000000000004420000000000000000fb0000000800540069006d0065010000000000000450000000000000000000000627000005dc00000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000
Selection:
collapsed: false
Tool Properties:
collapsed: false
Views:
collapsed: true
Width: 2518
X: 385
Y: 120

View File

@@ -77,9 +77,6 @@ class UrArmTask():
if n > retry: if n > retry:
raise Exception('Can not connect to the arm info server!') raise Exception('Can not connect to the arm info server!')
self.pose_data = {}
self.pose_file = 'C:\\auto\\unilabos\\unilabos\\devices\\agv\\pose.json'
self.reload_pose()
self.dash_c.stop() self.dash_c.stop()
def arm_init(self): def arm_init(self):

View File

@@ -36,6 +36,7 @@ class DPLiquidHandler(LiquidHandler):
delays: Optional[List[int]] = None, delays: Optional[List[int]] = None,
is_96_well: Optional[bool] = False, is_96_well: Optional[bool] = False,
top: Optional[List(float)] = None, top: Optional[List(float)] = None,
none_keys: List[str] = []
): ):
"""A complete *remove* (aspirate → waste) operation.""" """A complete *remove* (aspirate → waste) operation."""
trash = self.deck.get_trash_area() trash = self.deck.get_trash_area()
@@ -98,7 +99,8 @@ class DPLiquidHandler(LiquidHandler):
mix_time: Optional[int] = None, mix_time: Optional[int] = None,
mix_vol: Optional[int] = None, mix_vol: Optional[int] = None,
mix_rate: Optional[int] = None, mix_rate: Optional[int] = None,
mix_liquid_height: Optional[float] = None mix_liquid_height: Optional[float] = None,
none_keys: List[str] = []
): ):
"""A complete *add* (aspirate reagent → dispense into targets) operation.""" """A complete *add* (aspirate reagent → dispense into targets) operation."""
@@ -177,6 +179,7 @@ class DPLiquidHandler(LiquidHandler):
mix_rate: Optional[int] = None, mix_rate: Optional[int] = None,
mix_liquid_height: Optional[float] = None, mix_liquid_height: Optional[float] = None,
delays: Optional[List[int]] = None, delays: Optional[List[int]] = None,
none_keys: List[str] = []
): ):
"""Transfer liquid from each *source* well/plate to the corresponding *target*. """Transfer liquid from each *source* well/plate to the corresponding *target*.
@@ -295,6 +298,7 @@ class DPLiquidHandler(LiquidHandler):
height_to_bottom: Optional[float] = None, height_to_bottom: Optional[float] = None,
offsets: Optional[Coordinate] = None, offsets: Optional[Coordinate] = None,
mix_rate: Optional[float] = None, mix_rate: Optional[float] = None,
none_keys: List[str] = []
): ):
if mix_time is None: # No mixing required if mix_time is None: # No mixing required
return return

View File

View File

@@ -0,0 +1,208 @@
import copy
import rclpy
import json
import time
from rclpy.executors import MultiThreadedExecutor
from rclpy.action import ActionServer
from sensor_msgs.msg import JointState
from unilabos_msgs.action import SendCmd
from rclpy.action.server import ServerGoalHandle
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
from tf_transformations import quaternion_from_euler
from tf2_ros import TransformBroadcaster, Buffer, TransformListener
class LiquidHandlerJointPublisher(BaseROS2DeviceNode):
def __init__(self,device_id:str, joint_config:dict, lh_id:str,resource_tracker, rate=50):
super().__init__(
driver_instance=self,
device_id=device_id,
status_types={},
action_value_mappings={},
hardware_interface={},
print_publish=False,
resource_tracker=resource_tracker,
)
# joint_config_dict = {
# "joint_names":[
# "first_joint",
# "second_joint",
# "third_joint",
# "fourth_joint"
# ],
# "y":{
# "first_joint":{
# "factor":-1,
# "offset":0.0
# }
# },
# "x":{
# "second_joint":{
# "factor":-1,
# "offset":0.0
# }
# },
# "z":{
# "third_joint":{
# "factor":1,
# "offset":0.0
# },
# "fourth_joint":{
# "factor":1,
# "offset":0.0
# }
# }
# }
self.j_msg = JointState()
self.lh_id = lh_id
# self.j_msg.name = joint_names
self.joint_config = joint_config
self.j_msg.position = [0.0 for i in range(len(joint_config['joint_names']))]
self.j_msg.name = [f"{self.lh_id}_{x}" for x in joint_config['joint_names']]
# self.joint_config = joint_config_dict
# self.j_msg.position = [0.0 for i in range(len(joint_config_dict['joint_names']))]
# self.j_msg.name = [f"{self.lh_id}_{x}" for x in joint_config_dict['joint_names']]
self.rate = rate
self.tf_buffer = Buffer()
self.tf_listener = TransformListener(self.tf_buffer, self)
self.j_pub = self.create_publisher(JointState,'/joint_states',10)
self.create_timer(0.02,self.lh_joint_pub_callback)
self.j_action = ActionServer(
self,
SendCmd,
"joint",
self.lh_joint_action_callback,
result_timeout=5000
)
def lh_joint_action_callback(self,goal_handle: ServerGoalHandle):
"""Move a single joint
Args:
command: A JSON-formatted string that includes joint_name, speed, position
joint_name (str): The name of the joint to move
speed (float): The speed of the movement, speed > 0
position (float): The position to move to
Returns:
None
"""
result = SendCmd.Result()
cmd_str = str(goal_handle.request.command).replace('\'','\"')
# goal_handle.execute()
try:
cmd_dict = json.loads(cmd_str)
self.move_joints(**cmd_dict)
result.success = True
goal_handle.succeed()
except Exception as e:
print(e)
goal_handle.abort()
result.success = False
return result
def inverse_kinematics(self, x, y, z,
x_joint:dict,
y_joint:dict,
z_joint:dict ):
"""
将x、y、z坐标转换为对应关节的位置
Args:
x (float): x坐标
y (float): y坐标
z (float): z坐标
x_joint (dict): x轴关节配置包含plus和offset
y_joint (dict): y轴关节配置包含plus和offset
z_joint (dict): z轴关节配置包含plus和offset
Returns:
dict: 关节名称和对应位置的字典
"""
joint_positions = copy.deepcopy(self.j_msg.position)
# 处理x轴关节
for joint_name, config in x_joint.items():
index = self.j_msg.name.index(f"{self.lh_id}_{joint_name}")
joint_positions[index] = x * config["factor"] + config["offset"]
# 处理y轴关节
for joint_name, config in y_joint.items():
index = self.j_msg.name.index(f"{self.lh_id}_{joint_name}")
joint_positions[index] = y * config["factor"] + config["offset"]
# 处理z轴关节
for joint_name, config in z_joint.items():
index = self.j_msg.name.index(f"{self.lh_id}_{joint_name}")
joint_positions[index] = z * config["factor"] + config["offset"]
return joint_positions
def move_joints(self, resource_name, link_name, speed, x_joint=None, y_joint=None, z_joint=None):
transform = self.tf_buffer.lookup_transform(
link_name,
resource_name,
rclpy.time.Time()
)
x,y,z = transform.transform.translation.x, transform.transform.translation.y, transform.transform.translation.z
if x_joint is None:
x_joint_config = next(iter(self.joint_config['x'].items()))
elif x_joint in self.joint_config['x']:
x_joint_config = self.joint_config['x'][x_joint]
else:
raise ValueError(f"x_joint {x_joint} not in joint_config['x']")
if y_joint is None:
y_joint_config = next(iter(self.joint_config['y'].items()))
elif y_joint in self.joint_config['y']:
y_joint_config = self.joint_config['y'][y_joint]
else:
raise ValueError(f"y_joint {y_joint} not in joint_config['y']")
if z_joint is None:
z_joint_config = next(iter(self.joint_config['z'].items()))
elif z_joint in self.joint_config['z']:
z_joint_config = self.joint_config['z'][z_joint]
else:
raise ValueError(f"z_joint {z_joint} not in joint_config['z']")
joint_positions_target = self.inverse_kinematics(x,y,z,x_joint_config,y_joint_config,z_joint_config)
loop_flag = 0
while loop_flag < len(self.joint_config['joint_names']):
loop_flag = 0
for i in range(len(self.joint_config['joint_names'])):
distance = joint_positions_target[i] - self.j_msg.position[i]
if distance == 0:
loop_flag += 1
continue
minus_flag = distance/abs(distance)
if abs(distance) > speed/self.rate:
self.j_msg.position[i] += minus_flag * speed/self.rate
else :
self.j_msg.position[i] = joint_positions_target[i]
loop_flag += 1
# 发布关节状态
self.lh_joint_pub_callback()
time.sleep(1/self.rate)
def lh_joint_pub_callback(self):
self.j_msg.header.stamp = self.get_clock().now().to_msg()
self.j_pub.publish(self.j_msg)
def main():
pass
if __name__ == '__main__':
main()

View File

@@ -163,13 +163,134 @@ liquid_handler:
schema: schema:
type: object type: object
properties: properties:
status: name:
type: string type: string
description: 液体处理仪器当前状态 description: 液体处理仪器当前状态
required: required:
- status - name
additionalProperties: false additionalProperties: false
dp_liquid_handler:
description: 通用液体处理
class:
module: unilabos.devices.liquid_handling.action_definition:DPLiquidHandler
type: python
status_types:
status: String
action_value_mappings:
remove_liquid:
type: DPLiquidHandlerRemoveLiquid
goal:
vols: vols
sources: sources
waste_liquid: waste_liquid
use_channels: use_channels
flow_rates: flow_rates
offsets: offsets
liquid_height: liquid_height
blow_out_air_volume: blow_out_air_volume
spread: spread
delays: delays
is_96_well: is_96_well
top: top
none_keys: none_keys
feedback: {}
result: {}
add_liquid:
type: DPLiquidHandlerAddLiquid
goal:
asp_vols: asp_vols
dis_vols: dis_vols
reagent_sources: reagent_sources
targets: targets
use_channels: use_channels
flow_rates: flow_rates
offsets: offsets
liquid_height: liquid_height
blow_out_air_volume: blow_out_air_volume
spread: spread
is_96_well: is_96_well
mix_time: mix_time
mix_vol: mix_vol
mix_rate: mix_rate
mix_liquid_height: mix_liquid_height
none_keys: none_keys
feedback: {}
result: {}
transfer_liquid:
type: DPLiquidHandlerTransferLiquid
goal:
asp_vols: asp_vols
dis_vols: dis_vols
sources: sources
targets: targets
tip_racks: tip_racks
use_channels: use_channels
asp_flow_rates: asp_flow_rates
dis_flow_rates: dis_flow_rates
offsets: offsets
touch_tip: touch_tip
liquid_height: liquid_height
blow_out_air_volume: blow_out_air_volume
spread: spread
is_96_well: is_96_well
mix_stage: mix_stage
mix_times: mix_times
mix_vol: mix_vol
mix_rate: mix_rate
mix_liquid_height: mix_liquid_height
delays: delays
none_keys: none_keys
feedback: {}
result: {}
custom_delay:
type: DPLiquidHandlerCustomDelay
goal:
seconds: seconds
msg: msg
feedback: {}
result: {}
touch_tip:
type: DPLiquidHandlerTouchTip
goal:
targets: targets
feedback: {}
result: {}
mix:
type: DPLiquidHandlerMix
goal:
targets: targets
mix_time: mix_time
mix_vol: mix_vol
height_to_bottom: height_to_bottom
offsets: offsets
mix_rate: mix_rate
none_keys: none_keys
feedback: {}
result: {}
set_tiprack:
type: DPLiquidHandlerSetTiprack
goal:
tip_racks: tip_racks
feedback: {}
result: {}
move_to:
type: DPLiquidHandlerMoveTo
goal:
well: well
dis_to_top: dis_to_top
channel: channel
feedback: {}
result: {}
schema:
type: object
properties:
name:
type: string
description: 物料名
required:
- name
liquid_handler.revvity: liquid_handler.revvity:
class: class:
module: unilabos.devices.liquid_handling.revvity:Revvity module: unilabos.devices.liquid_handling.revvity:Revvity
@@ -179,7 +300,7 @@ liquid_handler.revvity:
action_value_mappings: action_value_mappings:
run: run:
type: WorkStationRun type: WorkStationRun
goal: goal:
wf_name: file_path wf_name: file_path
params: params params: params
resource: resource resource: resource
@@ -187,4 +308,3 @@ liquid_handler.revvity:
status: status status: status
result: result:
success: success success: success

View File

@@ -20,7 +20,6 @@ gripper.mock:
position: position position: position
effort: torque effort: torque
gripper.misumi_rz: gripper.misumi_rz:
description: Misumi RZ gripper description: Misumi RZ gripper
class: class:
@@ -35,4 +34,4 @@ gripper.misumi_rz:
command: command command: command
feedback: {} feedback: {}
result: result:
success: success success: success

View File

@@ -0,0 +1,5 @@
lh_joint_publisher:
class:
module: unilabos.devices.ros_dev.liquid_handler_joint_publisher:LiquidHandlerJointPublisher
type: ros2

View File

@@ -7,7 +7,7 @@ zhida_hplc:
status: String status: String
action_value_mappings: action_value_mappings:
start: start:
type: StringSingleInput type: StrSingleInput
goal: goal:
string: string string: string
feedback: {} feedback: {}

View File

@@ -1,3 +1,4 @@
import io
import json import json
import os import os
import sys import sys
@@ -20,7 +21,43 @@ class Registry:
self.registry_paths = DEFAULT_PATHS.copy() # 使用copy避免修改默认值 self.registry_paths = DEFAULT_PATHS.copy() # 使用copy避免修改默认值
if registry_paths: if registry_paths:
self.registry_paths.extend(registry_paths) self.registry_paths.extend(registry_paths)
self.device_type_registry = {} action_type = self._replace_type_with_class(
"ResourceCreateFromOuter", "host_node", f"动作 add_resource_from_outer"
)
schema = ros_action_to_json_schema(action_type)
self.device_type_registry = {
"host_node": {
"description": "UniLabOS主机节点",
"class": {
"module": "unilabos.ros.nodes.presets.host_node",
"type": "python",
"status_types": {},
"action_value_mappings": {
"add_resource_from_outer": {
"type": msg_converter_manager.search_class("ResourceCreateFromOuter"),
"goal": {
"resources": "resources",
"device_ids": "device_ids",
"bind_parent_ids": "bind_parent_ids",
"bind_locations": "bind_locations",
"other_calling_params": "other_calling_params",
},
"feedback": {},
"result": {
"success": "success"
},
"schema": schema
}
}
},
"schema": {
"properties": {},
"additionalProperties": False,
"type": "object"
},
"file_path": "/"
}
}
self.resource_type_registry = {} self.resource_type_registry = {}
self._setup_called = False # 跟踪setup是否已调用 self._setup_called = False # 跟踪setup是否已调用
# 其他状态变量 # 其他状态变量
@@ -107,6 +144,7 @@ class Registry:
+ f"total: {len(files)}" + f"total: {len(files)}"
) )
current_device_number = len(self.device_type_registry) + 1 current_device_number = len(self.device_type_registry) + 1
from unilabos.app.web.utils.action_utils import get_yaml_from_goal_type
for i, file in enumerate(files): for i, file in enumerate(files):
data = yaml.safe_load(open(file, encoding="utf-8")) data = yaml.safe_load(open(file, encoding="utf-8"))
if data: if data:
@@ -131,6 +169,7 @@ class Registry:
action_config["type"] = self._replace_type_with_class( action_config["type"] = self._replace_type_with_class(
action_config["type"], device_id, f"动作 {action_name}" action_config["type"], device_id, f"动作 {action_name}"
) )
action_config["goal_default"] = yaml.safe_load(io.StringIO(get_yaml_from_goal_type(action_config["type"].Goal)))
action_config["schema"] = ros_action_to_json_schema(action_config["type"]) action_config["schema"] = ros_action_to_json_schema(action_config["type"])
self.device_type_registry.update(data) self.device_type_registry.update(data)

View File

@@ -2,4 +2,7 @@ OTDeck:
description: Opentrons deck description: Opentrons deck
class: class:
module: pylabrobot.resources.opentrons.deck:OTDeck module: pylabrobot.resources.opentrons.deck:OTDeck
type: pylabrobot type: pylabrobot
model:
type: device
mesh: opentrons_liquid_handler

View File

@@ -39,6 +39,10 @@ nest_96_wellplate_2ml_deep:
class: class:
module: pylabrobot.resources.opentrons.plates:nest_96_wellplate_2ml_deep module: pylabrobot.resources.opentrons.plates:nest_96_wellplate_2ml_deep
type: pylabrobot type: pylabrobot
model:
type: resource
mesh: tecan_nested_tip_rack/meshes/plate.stl
mesh_tf: [0.064, 0.043, 0, -1.5708, 0, 1.5708]
nest_96_wellplate_200ul_flat: nest_96_wellplate_200ul_flat:
description: Nest 96 wellplate 200ul flat description: Nest 96 wellplate 200ul flat
@@ -51,6 +55,12 @@ nest_96_wellplate_100ul_pcr_full_skirt:
class: class:
module: pylabrobot.resources.opentrons.plates:nest_96_wellplate_100ul_pcr_full_skirt module: pylabrobot.resources.opentrons.plates:nest_96_wellplate_100ul_pcr_full_skirt
type: pylabrobot type: pylabrobot
model:
type: resource
mesh: tecan_nested_tip_rack/meshes/plate.stl
mesh_tf: [0.064, 0.043, 0, -1.5708, 0, 1.5708]
children_mesh: generic_labware_tube_10_75/meshes/0_base.stl
children_mesh_tf: [0.0018, 0.0018, 0, -1.5708,0, 0]
appliedbiosystemsmicroamp_384_wellplate_40ul: appliedbiosystemsmicroamp_384_wellplate_40ul:
description: Applied Biosystems microamp 384 wellplate 40ul description: Applied Biosystems microamp 384 wellplate 40ul

View File

@@ -63,7 +63,13 @@ opentrons_96_filtertiprack_1000ul:
class: class:
module: pylabrobot.resources.opentrons.tip_racks:opentrons_96_filtertiprack_1000ul module: pylabrobot.resources.opentrons.tip_racks:opentrons_96_filtertiprack_1000ul
type: pylabrobot type: pylabrobot
model:
type: resource
mesh: tecan_nested_tip_rack/meshes/plate.stl
mesh_tf: [0.064, 0.043, 0, -1.5708, 0, 1.5708]
children_mesh: generic_labware_tube_10_75/meshes/0_base.stl
children_mesh_tf: [0.0018, 0.0018, 0, -1.5708,0, 0]
opentrons_96_filtertiprack_20ul: opentrons_96_filtertiprack_20ul:
description: Opentrons 96 filtertiprack 20ul description: Opentrons 96 filtertiprack 20ul
class: class:

View File

@@ -394,14 +394,14 @@ def resource_ulab_to_plr(resource: dict, plr_model=False) -> "ResourcePLR":
return resource_plr return resource_plr
def resource_plr_to_ulab(resource_plr: "ResourcePLR"): def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None):
def resource_plr_to_ulab_inner(d: dict, all_states: dict) -> dict: def resource_plr_to_ulab_inner(d: dict, all_states: dict) -> dict:
r = { r = {
"id": d["name"], "id": d["name"],
"name": d["name"], "name": d["name"],
"sample_id": None, "sample_id": None,
"children": [resource_plr_to_ulab_inner(child, all_states) for child in d["children"]], "children": [resource_plr_to_ulab_inner(child, all_states) for child in d["children"]],
"parent": d["parent_name"] if d["parent_name"] else None, "parent": d["parent_name"] if d["parent_name"] else parent_name if parent_name else None,
"type": "device", # FIXME plr自带的type是python class name "type": "device", # FIXME plr自带的type是python class name
"class": d.get("class", ""), "class": d.get("class", ""),
"position": ( "position": (
@@ -417,6 +417,7 @@ def resource_plr_to_ulab(resource_plr: "ResourcePLR"):
d = resource_plr.serialize() d = resource_plr.serialize()
all_states = resource_plr.serialize_all_state() all_states = resource_plr.serialize_all_state()
r = resource_plr_to_ulab_inner(d, all_states) r = resource_plr_to_ulab_inner(d, all_states)
return r return r
@@ -451,7 +452,8 @@ def initialize_resource(resource_config: dict, lab_registry: dict) -> list[dict]
if resource_class_config["type"] == "pylabrobot": if resource_class_config["type"] == "pylabrobot":
resource_plr = RESOURCE(name=resource_config["name"]) resource_plr = RESOURCE(name=resource_config["name"])
r = resource_plr_to_ulab(resource_plr=resource_plr) r = resource_plr_to_ulab(resource_plr=resource_plr, parent_name=resource_config.get("parent", None))
# r = resource_plr_to_ulab(resource_plr=resource_plr)
if resource_config.get("position") is not None: if resource_config.get("position") is not None:
r["position"] = resource_config["position"] r["position"] = resource_config["position"]
r = tree_to_list([r]) r = tree_to_list([r])
@@ -475,8 +477,10 @@ def initialize_resources(resources_config) -> list[dict]:
""" """
from unilabos.registry.registry import lab_registry from unilabos.registry.registry import lab_registry
resources = [] resources = []
for resource_config in resources_config: for resource_config in resources_config:
if resource_config["parent"] == "tip_rack" or resource_config["parent"] == "plate_well":
continue
resources.extend(initialize_resource(resource_config, lab_registry)) resources.extend(initialize_resource(resource_config, lab_registry))
return resources return resources

View File

@@ -1,5 +1,5 @@
import sys import traceback
from unilabos.utils.log import logger
resource_schema = { resource_schema = {
"workstation": {"type": "object", "properties": {}}, "workstation": {"type": "object", "properties": {}},
@@ -132,7 +132,8 @@ def add_schema(resources_config: list[dict]) -> list[dict]:
try: try:
if type(resource["children"][0]) == dict: if type(resource["children"][0]) == dict:
resource["children"] = add_schema(resource["children"]) resource["children"] = add_schema(resource["children"])
except: except Exception as ex:
sys.exit(0) logger.error("添加物料schema时出错")
traceback.print_exc()
return resources_config return resources_config

View File

@@ -18,6 +18,7 @@ class ROS2DeviceNodeWrapper(ROS2DeviceNode):
def ros2_device_node( def ros2_device_node(
cls: Type[T], cls: Type[T],
device_config: Optional[Dict[str, Any]] = None,
status_types: Optional[Dict[str, Any]] = None, status_types: Optional[Dict[str, Any]] = None,
action_value_mappings: Optional[Dict[str, Any]] = None, action_value_mappings: Optional[Dict[str, Any]] = None,
hardware_interface: Optional[Dict[str, Any]] = None, hardware_interface: Optional[Dict[str, Any]] = None,
@@ -30,6 +31,7 @@ def ros2_device_node(
cls: 要封装的设备类 cls: 要封装的设备类
status_types: 需要发布的状态和传感器信息,每个(PROP: TYPE)PROP应该匹配cls.PROP或cls.get_PROP() status_types: 需要发布的状态和传感器信息,每个(PROP: TYPE)PROP应该匹配cls.PROP或cls.get_PROP()
TYPE应该是ROS2消息类型。默认为{} TYPE应该是ROS2消息类型。默认为{}
device_config: 初始化时的config。
action_value_mappings: 设备动作。默认为{} action_value_mappings: 设备动作。默认为{}
每个(ACTION: {'type': CMD_TYPE, 'goal': {FIELD: PROP}, 'feedback': {FIELD: PROP}, 'result': {FIELD: PROP}}), 每个(ACTION: {'type': CMD_TYPE, 'goal': {FIELD: PROP}, 'feedback': {FIELD: PROP}, 'result': {FIELD: PROP}}),
hardware_interface: 硬件接口配置。默认为{"name": "hardware_interface", "write": "send_command", "read": "read_data", "extra_info": []}。 hardware_interface: 硬件接口配置。默认为{"name": "hardware_interface", "write": "send_command", "read": "read_data", "extra_info": []}。
@@ -42,6 +44,8 @@ def ros2_device_node(
# 从属性中自动发现可发布状态 # 从属性中自动发现可发布状态
if status_types is None: if status_types is None:
status_types = {} status_types = {}
if device_config is None:
device_config = {}
if action_value_mappings is None: if action_value_mappings is None:
action_value_mappings = {} action_value_mappings = {}
if hardware_interface is None: if hardware_interface is None:
@@ -73,6 +77,7 @@ def ros2_device_node(
"__init__": lambda self, *args, **kwargs: init_wrapper( "__init__": lambda self, *args, **kwargs: init_wrapper(
self, self,
driver_class=cls, driver_class=cls,
device_config=device_config,
status_types=status_types, status_types=status_types,
action_value_mappings=action_value_mappings, action_value_mappings=action_value_mappings,
hardware_interface=hardware_interface, hardware_interface=hardware_interface,

View File

@@ -1,9 +1,9 @@
import rclpy import copy
from rclpy.node import Node
from typing import Optional from typing import Optional
from unilabos.registry.registry import lab_registry from unilabos.registry.registry import lab_registry
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, DeviceInitError
from unilabos.ros.device_node_wrapper import ros2_device_node from unilabos.ros.device_node_wrapper import ros2_device_node
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, DeviceInitError
from unilabos.utils import logger from unilabos.utils import logger
from unilabos.utils.import_manager import default_manager from unilabos.utils.import_manager import default_manager
@@ -22,17 +22,21 @@ def initialize_device_from_dict(device_id, device_config) -> Optional[ROS2Device
None None
""" """
d = None d = None
original_device_config = copy.deepcopy(device_config)
device_class_config = device_config["class"] device_class_config = device_config["class"]
if isinstance(device_class_config, str): # 如果是字符串则直接去lab_registry中查找获取class if isinstance(device_class_config, str): # 如果是字符串则直接去lab_registry中查找获取class
if device_class_config not in lab_registry.device_type_registry: if device_class_config not in lab_registry.device_type_registry:
raise ValueError(f"Device class {device_class_config} not found.") raise ValueError(f"Device class {device_class_config} not found.")
device_class_config = device_config["class"] = lab_registry.device_type_registry[device_class_config]["class"] device_class_config = device_config["class"] = lab_registry.device_type_registry[device_class_config]["class"]
else:
raise ValueError("不再支持class为字典传入class必须为注册表中已经提供的设备您可以新增注册表并通过--registry传入")
if isinstance(device_class_config, dict): if isinstance(device_class_config, dict):
DEVICE = default_manager.get_class(device_class_config["module"]) DEVICE = default_manager.get_class(device_class_config["module"])
# 不管是ros2的实例还是python的都必须包一次除了HostNode # 不管是ros2的实例还是python的都必须包一次除了HostNode
DEVICE = ros2_device_node( DEVICE = ros2_device_node(
DEVICE, DEVICE,
status_types=device_class_config.get("status_types", {}), status_types=device_class_config.get("status_types", {}),
device_config=original_device_config,
action_value_mappings=device_class_config.get("action_value_mappings", {}), action_value_mappings=device_class_config.get("action_value_mappings", {}),
hardware_interface=device_class_config.get( hardware_interface=device_class_config.get(
"hardware_interface", "hardware_interface",

View File

@@ -2,9 +2,13 @@ import copy
import json import json
import os import os
import threading import threading
import time
from typing import Optional, Dict, Any, List from typing import Optional, Dict, Any, List
import rclpy import rclpy
from unilabos.ros.nodes.presets.joint_republisher import JointRepublisher
from unilabos.ros.nodes.presets.resource_mesh_manager import ResourceMeshManager
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
from unilabos_msgs.msg import Resource # type: ignore from unilabos_msgs.msg import Resource # type: ignore
from unilabos_msgs.srv import ResourceAdd, SerialCommand # type: ignore from unilabos_msgs.srv import ResourceAdd, SerialCommand # type: ignore
from rclpy.executors import MultiThreadedExecutor from rclpy.executors import MultiThreadedExecutor
@@ -40,17 +44,19 @@ def exit() -> None:
def main( def main(
devices_config: Dict[str, Any] = {}, devices_config: Dict[str, Any] = {},
resources_config={}, resources_config: list=[],
graph: Optional[Dict[str, Any]] = None, graph: Optional[Dict[str, Any]] = None,
controllers_config: Dict[str, Any] = {}, controllers_config: Dict[str, Any] = {},
bridges: List[Any] = [], bridges: List[Any] = [],
args: List[str] = ["--log-level", "debug"], visual: str = "disable",
resources_mesh_config: dict = {},
rclpy_init_args: List[str] = ["--log-level", "debug"],
discovery_interval: float = 5.0, discovery_interval: float = 5.0,
) -> None: ) -> None:
"""主函数""" """主函数"""
rclpy.init(args=args)
rclpy.__executor = executor = MultiThreadedExecutor()
rclpy.init(args=rclpy_init_args)
executor = rclpy.__executor = MultiThreadedExecutor()
# 创建主机节点 # 创建主机节点
host_node = HostNode( host_node = HostNode(
"host_node", "host_node",
@@ -62,11 +68,26 @@ def main(
discovery_interval, discovery_interval,
) )
if visual != "disable":
resource_mesh_manager = ResourceMeshManager(
resources_mesh_config,
resources_config,
resource_tracker= DeviceNodeResourceTracker(),
device_id = 'resource_mesh_manager',
)
joint_republisher = JointRepublisher(
'joint_republisher',
DeviceNodeResourceTracker()
)
executor.add_node(resource_mesh_manager)
executor.add_node(joint_republisher)
thread = threading.Thread(target=executor.spin, daemon=True, name="host_executor_thread") thread = threading.Thread(target=executor.spin, daemon=True, name="host_executor_thread")
thread.start() thread.start()
while True: while True:
input() time.sleep(1)
def slave( def slave(
@@ -75,11 +96,16 @@ def slave(
graph: Optional[Dict[str, Any]] = None, graph: Optional[Dict[str, Any]] = None,
controllers_config: Dict[str, Any] = {}, controllers_config: Dict[str, Any] = {},
bridges: List[Any] = [], bridges: List[Any] = [],
args: List[str] = ["--log-level", "debug"], visual: str = "disable",
resources_mesh_config: dict = {},
rclpy_init_args: List[str] = ["--log-level", "debug"],
) -> None: ) -> None:
"""从节点函数""" """从节点函数"""
rclpy.init(args=args) if not rclpy.ok():
rclpy.__executor = executor = MultiThreadedExecutor() rclpy.init(args=rclpy_init_args)
executor = rclpy.__executor
if not executor:
executor = rclpy.__executor = MultiThreadedExecutor()
devices_config_copy = copy.deepcopy(devices_config) devices_config_copy = copy.deepcopy(devices_config)
for device_id, device_config in devices_config.items(): for device_id, device_config in devices_config.items():
d = initialize_device_from_dict(device_id, device_config) d = initialize_device_from_dict(device_id, device_config)
@@ -94,6 +120,21 @@ def slave(
n = Node(f"slaveMachine_{BasicConfig.machine_name}", parameter_overrides=[]) n = Node(f"slaveMachine_{BasicConfig.machine_name}", parameter_overrides=[])
executor.add_node(n) executor.add_node(n)
if visual != "disable":
resource_mesh_manager = ResourceMeshManager(
resources_mesh_config,
resources_config,
resource_tracker= DeviceNodeResourceTracker(),
device_id = 'resource_mesh_manager',
)
joint_republisher = JointRepublisher(
'joint_republisher',
DeviceNodeResourceTracker()
)
executor.add_node(resource_mesh_manager)
executor.add_node(joint_republisher)
thread = threading.Thread(target=executor.spin, daemon=True, name="slave_executor_thread") thread = threading.Thread(target=executor.spin, daemon=True, name="slave_executor_thread")
thread.start() thread.start()
@@ -112,7 +153,7 @@ def slave(
logger.info(f"Slave node info updated.") logger.info(f"Slave node info updated.")
rclient = n.create_client(ResourceAdd, "/resources/add") rclient = n.create_client(ResourceAdd, "/resources/add")
rclient.wait_for_service() # FIXME 可能一直等待,加一个参数 rclient.wait_for_service()
request = ResourceAdd.Request() request = ResourceAdd.Request()
request.resources = [convert_to_ros_msg(Resource, resource) for resource in resources_config] request.resources = [convert_to_ros_msg(Resource, resource) for resource in resources_config]
@@ -120,7 +161,7 @@ def slave(
logger.info(f"Slave resource added.") logger.info(f"Slave resource added.")
while True: while True:
input() time.sleep(1)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -133,15 +133,15 @@ _msg_converter: Dict[Type, Any] = {
String: lambda x: String(data=str(x)), String: lambda x: String(data=str(x)),
Point: lambda x: Point(x=x.x, y=x.y, z=x.z), Point: lambda x: Point(x=x.x, y=x.y, z=x.z),
Resource: lambda x: Resource( Resource: lambda x: Resource(
id=x["id"], id=x.get("id", ""),
name=x["name"], name=x.get("name", ""),
sample_id=x.get("sample_id", "") or "", sample_id=x.get("sample_id", "") or "",
children=list(x.get("children", [])), children=list(x.get("children", [])),
parent=x.get("parent", "") or "", parent=x.get("parent", "") or "",
type=x["type"], type=x.get("type", ""),
category=x.get("class", "") or x["type"], category=x.get("class", "") or x.get("type", ""),
pose=( pose=(
Pose(position=Point(x=float(x["position"]["x"]), y=float(x["position"]["y"]), z=float(x["position"]["z"]))) Pose(position=Point(x=float(x.get("position", {}).get("x", 0)), y=float(x.get("position", {}).get("y", 0)), z=float(x.get("position", {}).get("z", 0))))
if x.get("position", None) is not None if x.get("position", None) is not None
else Pose() else Pose()
), ),
@@ -331,16 +331,27 @@ def convert_to_ros_msg(ros_msg_type: Union[Type, Any], obj: Any) -> Any:
ros_msg = ros_msg_type() if isinstance(ros_msg_type, type) else ros_msg_type ros_msg = ros_msg_type() if isinstance(ros_msg_type, type) else ros_msg_type
# 提取数据 # 提取数据
data = _extract_data(obj) extract_data = dict(_extract_data(obj))
# 转换数据到ROS消息 # 转换数据到ROS消息
for key, value in data.items(): for ind, data in enumerate(ros_msg.get_fields_and_field_types().items()):
key, type_name = data
if key not in extract_data:
continue
value = extract_data[key]
if hasattr(ros_msg, key): if hasattr(ros_msg, key):
attr = getattr(ros_msg, key) attr = getattr(ros_msg, key)
if isinstance(attr, (float, int, str, bool)): if isinstance(attr, (float, int, str, bool)):
setattr(ros_msg, key, value) setattr(ros_msg, key, value)
elif isinstance(attr, (list, tuple)) and isinstance(value, Iterable): elif isinstance(attr, (list, tuple)) and isinstance(value, Iterable):
setattr(ros_msg, key, list(value)) td = ros_msg.SLOT_TYPES[ind].value_type
if isinstance(td, NamespacedType):
target_class = msg_converter_manager.get_class(f"{'.'.join(td.namespaces)}.{td.name}")
setattr(ros_msg, key, [convert_to_ros_msg(target_class, v) for v in value])
else:
setattr(ros_msg, key, []) # FIXME
elif "array.array" in str(type(attr)):
setattr(ros_msg, key, value)
else: else:
nested_ros_msg = convert_to_ros_msg(type(attr)(), value) nested_ros_msg = convert_to_ros_msg(type(attr)(), value)
setattr(ros_msg, key, nested_ros_msg) setattr(ros_msg, key, nested_ros_msg)
@@ -566,6 +577,7 @@ basic_type_map = {
'float32': {'type': 'number'}, 'float32': {'type': 'number'},
'float64': {'type': 'number'}, 'float64': {'type': 'number'},
'string': {'type': 'string'}, 'string': {'type': 'string'},
'boolean': {'type': 'boolean'},
'char': {'type': 'string', 'maxLength': 1}, 'char': {'type': 'string', 'maxLength': 1},
'byte': {'type': 'integer', 'minimum': 0, 'maximum': 255}, 'byte': {'type': 'integer', 'minimum': 0, 'maximum': 255},
} }

View File

@@ -10,13 +10,16 @@ import asyncio
import rclpy import rclpy
from rclpy.node import Node from rclpy.node import Node
from rclpy.action import ActionServer from rclpy.action import ActionServer, ActionClient
from rclpy.action.server import ServerGoalHandle from rclpy.action.server import ServerGoalHandle
from rclpy.client import Client from rclpy.client import Client
from rclpy.callback_groups import ReentrantCallbackGroup from rclpy.callback_groups import ReentrantCallbackGroup
from rclpy.service import Service from rclpy.service import Service
from unilabos_msgs.action import SendCmd
from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
from unilabos.resources.graphio import convert_resources_to_type, convert_resources_from_type from unilabos.resources.graphio import convert_resources_to_type, convert_resources_from_type, resource_ulab_to_plr, \
initialize_resources
from unilabos.ros.msgs.message_converter import ( from unilabos.ros.msgs.message_converter import (
convert_to_ros_msg, convert_to_ros_msg,
convert_from_ros_msg, convert_from_ros_msg,
@@ -101,6 +104,7 @@ def init_wrapper(
self, self,
device_id: str, device_id: str,
driver_class: type[T], driver_class: type[T],
device_config: Dict[str, Any],
status_types: Dict[str, Any], status_types: Dict[str, Any],
action_value_mappings: Dict[str, Any], action_value_mappings: Dict[str, Any],
hardware_interface: Dict[str, Any], hardware_interface: Dict[str, Any],
@@ -118,6 +122,7 @@ def init_wrapper(
children = [] children = []
kwargs["device_id"] = device_id kwargs["device_id"] = device_id
kwargs["driver_class"] = driver_class kwargs["driver_class"] = driver_class
kwargs["device_config"] = device_config
kwargs["driver_params"] = driver_params kwargs["driver_params"] = driver_params
kwargs["status_types"] = status_types kwargs["status_types"] = status_types
kwargs["action_value_mappings"] = action_value_mappings kwargs["action_value_mappings"] = action_value_mappings
@@ -302,10 +307,77 @@ class BaseROS2DeviceNode(Node, Generic[T]):
res.response = "" res.response = ""
return res return res
def append_resource(req: SerialCommand_Request, res: SerialCommand_Response):
# 物料传输到对应的node节点
rclient = self.create_client(ResourceAdd, "/resources/add")
rclient.wait_for_service()
request = ResourceAdd.Request()
command_json = json.loads(req.command)
namespace = command_json["namespace"]
bind_parent_id = command_json["bind_parent_id"]
edge_device_id = command_json["edge_device_id"]
location = command_json["bind_location"]
other_calling_param = command_json["other_calling_param"]
resources = command_json["resource"]
initialize_full = other_calling_param.pop("initialize_full", False)
# 本地拿到这个物料,可能需要先做初始化?
if isinstance(resources, list):
if initialize_full:
resources = initialize_resources(resources)
request.resources = [convert_to_ros_msg(Resource, resource) for resource in resources]
else:
if initialize_full:
resources = initialize_resources([resources])
request.resources = [convert_to_ros_msg(Resource, resources)]
response = rclient.call(request)
# 应该先add_resource了
res.response = "OK"
# 接下来该根据bind_parent_id进行assign了目前只有plr可以进行assign不然没有办法输入到物料系统中
resource = self.resource_tracker.figure_resource({"name": bind_parent_id})
request.resources = [convert_to_ros_msg(Resource, resources)]
try:
from pylabrobot.resources.resource import Resource as ResourcePLR
from pylabrobot.resources.deck import Deck
from pylabrobot.resources import Coordinate
from pylabrobot.resources import OTDeck
contain_model = not isinstance(resource, Deck)
if isinstance(resource, ResourcePLR):
# resources.list()
plr_instance = resource_ulab_to_plr(resources, contain_model)
if isinstance(resource, OTDeck) and "slot" in other_calling_param:
resource.assign_child_at_slot(plr_instance, **other_calling_param)
resource.assign_child_resource(plr_instance, Coordinate(location["x"], location["y"], location["z"]), **other_calling_param)
# 发送给ResourceMeshManager
action_client = ActionClient(
self, SendCmd, "/devices/resource_mesh_manager/add_resource_mesh", callback_group=self.callback_group
)
goal = SendCmd.Goal()
goal.command = json.dumps({
"resources": resources,
"bind_parent_id": bind_parent_id,
})
future = action_client.send_goal_async(goal, goal_uuid=uuid.uuid4())
def done_cb(*args):
self.lab_logger().info(f"向meshmanager发送新增resource完成")
future.add_done_callback(done_cb)
except ImportError:
self.lab_logger().error("Host请求添加物料时本环境并不存在pylabrobot")
except Exception as e:
self.lab_logger().error("Host请求添加物料时出错")
self.lab_logger().error(traceback.format_exc())
return res
# noinspection PyTypeChecker
self._service_server: Dict[str, Service] = { self._service_server: Dict[str, Service] = {
"query_host_name": self.create_service( "query_host_name": self.create_service(
SerialCommand, f"/srv{self.namespace}/query_host_name", query_host_name_cb, callback_group=self.callback_group SerialCommand, f"/srv{self.namespace}/query_host_name", query_host_name_cb, callback_group=self.callback_group
), ),
"append_resource": self.create_service(
SerialCommand, f"/srv{self.namespace}/append_resource", append_resource, callback_group=self.callback_group
),
} }
# 向全局在线设备注册表添加设备信息 # 向全局在线设备注册表添加设备信息
@@ -437,26 +509,36 @@ class BaseROS2DeviceNode(Node, Generic[T]):
action_kwargs = convert_from_ros_msg_with_mapping(goal, action_value_mapping["goal"]) action_kwargs = convert_from_ros_msg_with_mapping(goal, action_value_mapping["goal"])
self.lab_logger().debug(f"接收到原始目标: {action_kwargs}") self.lab_logger().debug(f"接收到原始目标: {action_kwargs}")
# 向Host查询物料当前状态如果是host本身的增加物料的请求则直接跳过
# 向Host查询物料当前状态 if action_name != "add_resource_from_outer":
for k, v in goal.get_fields_and_field_types().items(): for k, v in goal.get_fields_and_field_types().items():
if v in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]: if v in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]:
self.lab_logger().info(f"查询资源状态: Key: {k} Type: {v}") self.lab_logger().info(f"查询资源状态: Key: {k} Type: {v}")
try: current_resources = []
r = ResourceGet.Request() try:
r.id = action_kwargs[k]["id"] if v == "unilabos_msgs/Resource" else action_kwargs[k][0]["id"] if len(action_kwargs[k]) > 1:
r.with_children = True for i in action_kwargs[k]:
response = await self._resource_clients["resource_get"].call_async(r) r = ResourceGet.Request()
except Exception: r.id = i["id"]
logger.error(f"资源查询失败,默认使用本地资源") r.with_children = True
# 删除对response.resources的检查因为它总是存在 response = await self._resource_clients["resource_get"].call_async(r)
resources_list = [convert_from_ros_msg(rs) for rs in response.resources] # type: ignore # FIXME current_resources.extend(response.resources)
self.lab_logger().debug(f"资源查询结果: {len(resources_list)} 个资源") else:
type_hint = action_paramtypes[k] r = ResourceGet.Request()
final_type = get_type_class(type_hint) r.id = action_kwargs[k]["id"] if v == "unilabos_msgs/Resource" else action_kwargs[k][0]["id"]
# 判断 ACTION 是否需要特殊的物料类型如 pylabrobot.resources.Resource并做转换 r.with_children = True
final_resource = convert_resources_to_type(resources_list, final_type) response = await self._resource_clients["resource_get"].call_async(r)
action_kwargs[k] = self.resource_tracker.figure_resource(final_resource) current_resources.extend(response.resources)
except Exception:
logger.error(f"资源查询失败,默认使用本地资源")
# 删除对response.resources的检查因为它总是存在
resources_list = [convert_from_ros_msg(rs) for rs in current_resources] # type: ignore # FIXME
self.lab_logger().debug(f"资源查询结果: {len(resources_list)} 个资源")
type_hint = action_paramtypes[k]
final_type = get_type_class(type_hint)
# 判断 ACTION 是否需要特殊的物料类型如 pylabrobot.resources.Resource并做转换
final_resource = [convert_resources_to_type([i], final_type)[0] for i in resources_list]
action_kwargs[k] = self.resource_tracker.figure_resource(final_resource)
self.lab_logger().info(f"准备执行: {action_kwargs}, 函数: {ACTION.__name__}") self.lab_logger().info(f"准备执行: {action_kwargs}, 函数: {ACTION.__name__}")
time_start = time.time() time_start = time.time()
@@ -527,27 +609,28 @@ class BaseROS2DeviceNode(Node, Generic[T]):
del future del future
# 向Host更新物料当前状态 # 向Host更新物料当前状态
for k, v in goal.get_fields_and_field_types().items(): if action_name != "add_resource_from_outer":
if v not in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]: for k, v in goal.get_fields_and_field_types().items():
continue if v not in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]:
self.lab_logger().info(f"更新资源状态: {k}") continue
r = ResourceUpdate.Request() self.lab_logger().info(f"更新资源状态: {k}")
# 仅当action_kwargs[k]不为None时尝试转换 r = ResourceUpdate.Request()
akv = action_kwargs[k] # 仅当action_kwargs[k]不为None时尝试转换
apv = action_paramtypes[k] akv = action_kwargs[k]
final_type = get_type_class(apv) apv = action_paramtypes[k]
if final_type is None: final_type = get_type_class(apv)
continue if final_type is None:
try: continue
r.resources = [ try:
convert_to_ros_msg(Resource, self.resource_tracker.root_resource(rs)) r.resources = [
for rs in convert_resources_from_type(akv, final_type) # type: ignore # FIXME # 考虑反查到最大的 convert_to_ros_msg(Resource, self.resource_tracker.root_resource(rs))
] for rs in convert_resources_from_type(akv, final_type) # type: ignore # FIXME # 考虑反查到最大的
response = await self._resource_clients["resource_update"].call_async(r) ]
self.lab_logger().debug(f"资源更新结果: {response}") response = await self._resource_clients["resource_update"].call_async(r)
except Exception as e: self.lab_logger().debug(f"资源更新结果: {response}")
self.lab_logger().error(f"资源更新失败: {e}") except Exception as e:
self.lab_logger().error(traceback.format_exc()) self.lab_logger().error(f"资源更新失败: {e}")
self.lab_logger().error(traceback.format_exc())
# 发布结果 # 发布结果
goal_handle.succeed() goal_handle.succeed()
@@ -627,6 +710,7 @@ class ROS2DeviceNode:
self, self,
device_id: str, device_id: str,
driver_class: Type[T], driver_class: Type[T],
device_config: Dict[str, Any],
driver_params: Dict[str, Any], driver_params: Dict[str, Any],
status_types: Dict[str, Any], status_types: Dict[str, Any],
action_value_mappings: Dict[str, Any], action_value_mappings: Dict[str, Any],
@@ -641,6 +725,8 @@ class ROS2DeviceNode:
Args: Args:
device_id: 设备标识符 device_id: 设备标识符
driver_class: 设备类 driver_class: 设备类
device_config: 原始初始化的json
driver_params: driver初始化的参数
status_types: 状态类型映射 status_types: 状态类型映射
action_value_mappings: 动作值映射 action_value_mappings: 动作值映射
hardware_interface: 硬件接口配置 hardware_interface: 硬件接口配置
@@ -657,11 +743,12 @@ class ROS2DeviceNode:
# 保存设备类是否支持异步上下文 # 保存设备类是否支持异步上下文
self._has_async_context = hasattr(driver_class, "__aenter__") and hasattr(driver_class, "__aexit__") self._has_async_context = hasattr(driver_class, "__aenter__") and hasattr(driver_class, "__aexit__")
self._driver_class = driver_class self._driver_class = driver_class
self.device_config = device_config
self.driver_is_ros = driver_is_ros self.driver_is_ros = driver_is_ros
self.resource_tracker = DeviceNodeResourceTracker() self.resource_tracker = DeviceNodeResourceTracker()
# use_pylabrobot_creator 使用 cls的包路径检测 # use_pylabrobot_creator 使用 cls的包路径检测
use_pylabrobot_creator = driver_class.__module__.startswith("pylabrobot") use_pylabrobot_creator = driver_class.__module__.startswith("pylabrobot") or driver_class.__name__ == "DPLiquidHandler"
# TODO: 要在创建之前预先请求服务器是否有当前id的物料放到resource_tracker中让pylabrobot进行创建 # TODO: 要在创建之前预先请求服务器是否有当前id的物料放到resource_tracker中让pylabrobot进行创建
# 创建设备类实例 # 创建设备类实例

View File

@@ -7,11 +7,13 @@ import uuid
from typing import Optional, Dict, Any, List, ClassVar, Set from typing import Optional, Dict, Any, List, ClassVar, Set
from action_msgs.msg import GoalStatus from action_msgs.msg import GoalStatus
from unilabos_msgs.msg import Resource # type: ignore from geometry_msgs.msg import Point
from unilabos_msgs.srv import ResourceAdd, ResourceGet, ResourceDelete, ResourceUpdate, ResourceList, SerialCommand # type: ignore
from rclpy.action import ActionClient, get_action_server_names_and_types_by_node from rclpy.action import ActionClient, get_action_server_names_and_types_by_node
from rclpy.callback_groups import ReentrantCallbackGroup from rclpy.callback_groups import ReentrantCallbackGroup
from rclpy.service import Service from rclpy.service import Service
from unilabos_msgs.msg import Resource # type: ignore
from unilabos_msgs.srv import ResourceAdd, ResourceGet, ResourceDelete, ResourceUpdate, ResourceList, \
SerialCommand # type: ignore
from unique_identifier_msgs.msg import UUID from unique_identifier_msgs.msg import UUID
from unilabos.registry.registry import lab_registry from unilabos.registry.registry import lab_registry
@@ -23,11 +25,9 @@ from unilabos.ros.msgs.message_converter import (
convert_from_ros_msg, convert_from_ros_msg,
convert_to_ros_msg, convert_to_ros_msg,
msg_converter_manager, msg_converter_manager,
ros_action_to_json_schema,
) )
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, ROS2DeviceNode, DeviceNodeResourceTracker from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, ROS2DeviceNode, DeviceNodeResourceTracker
from unilabos.ros.nodes.presets.controller_node import ControllerNode from unilabos.ros.nodes.presets.controller_node import ControllerNode
from unilabos.utils.type_check import TypeEncoder
class HostNode(BaseROS2DeviceNode): class HostNode(BaseROS2DeviceNode):
@@ -50,7 +50,7 @@ class HostNode(BaseROS2DeviceNode):
self, self,
device_id: str, device_id: str,
devices_config: Dict[str, Any], devices_config: Dict[str, Any],
resources_config: Any, resources_config: list,
physical_setup_graph: Optional[Dict[str, Any]] = None, physical_setup_graph: Optional[Dict[str, Any]] = None,
controllers_config: Optional[Dict[str, Any]] = None, controllers_config: Optional[Dict[str, Any]] = None,
bridges: Optional[List[Any]] = None, bridges: Optional[List[Any]] = None,
@@ -76,7 +76,7 @@ class HostNode(BaseROS2DeviceNode):
driver_instance=self, driver_instance=self,
device_id=device_id, device_id=device_id,
status_types={}, status_types={},
action_value_mappings={}, action_value_mappings=lab_registry.device_type_registry["host_node"]["class"]["action_value_mappings"],
hardware_interface={}, hardware_interface={},
print_publish=False, print_publish=False,
resource_tracker=DeviceNodeResourceTracker(), # host node并不是通过initialize 包一层传进来的 resource_tracker=DeviceNodeResourceTracker(), # host node并不是通过initialize 包一层传进来的
@@ -97,15 +97,13 @@ class HostNode(BaseROS2DeviceNode):
self.bridges = bridges self.bridges = bridges
# 创建设备、动作客户端和目标存储 # 创建设备、动作客户端和目标存储
self.devices_names: Dict[str, str] = {} # 存储设备名称和命名空间的映射 self.devices_names: Dict[str, str] = {device_id: self.namespace} # 存储设备名称和命名空间的映射
self.devices_instances: Dict[str, ROS2DeviceNode] = {} # 存储设备实例 self.devices_instances: Dict[str, ROS2DeviceNode] = {} # 存储设备实例
self.device_machine_names: Dict[str, str] = {device_id: "本地", } # 存储设备ID到机器名称的映射 self.device_machine_names: Dict[str, str] = {device_id: "本地", } # 存储设备ID到机器名称的映射
self._action_clients: Dict[str, ActionClient] = {} # 用来存储多个ActionClient实例 self._action_clients: Dict[str, ActionClient] = {} # 用来存储多个ActionClient实例
self._action_value_mappings: Dict[str, Dict] = ( self._action_value_mappings: Dict[str, Dict] = {} # 用来存储多个ActionClient的type, goal, feedback, result的变量名映射关系
{}
) # 用来存储多个ActionClient的type, goal, feedback, result的变量名映射关系
self._goals: Dict[str, Any] = {} # 用来存储多个目标的状态 self._goals: Dict[str, Any] = {} # 用来存储多个目标的状态
self._online_devices: Set[str] = set() # 用于跟踪在线设备 self._online_devices: Set[str] = {f"{self.namespace}/{device_id}"} # 用于跟踪在线设备
self._last_discovery_time = 0.0 # 上次设备发现的时间 self._last_discovery_time = 0.0 # 上次设备发现的时间
self._discovery_lock = threading.Lock() # 设备发现的互斥锁 self._discovery_lock = threading.Lock() # 设备发现的互斥锁
self._subscribed_topics = set() # 用于跟踪已订阅的话题 self._subscribed_topics = set() # 用于跟踪已订阅的话题
@@ -259,16 +257,41 @@ class HostNode(BaseROS2DeviceNode):
self.lab_logger().debug(f"[Host Node] Created ActionClient (Discovery): {action_id}") self.lab_logger().debug(f"[Host Node] Created ActionClient (Discovery): {action_id}")
action_name = action_id[len(namespace) + 1:] action_name = action_id[len(namespace) + 1:]
edge_device_id = namespace[9:] edge_device_id = namespace[9:]
from unilabos.app.mq import mqtt_client # from unilabos.app.mq import mqtt_client
info_with_schema = ros_action_to_json_schema(action_type) # info_with_schema = ros_action_to_json_schema(action_type)
mqtt_client.publish_actions(action_name, { # mqtt_client.publish_actions(action_name, {
"device_id": edge_device_id, # "device_id": edge_device_id,
"action_name": action_name, # "device_type": "",
"schema": info_with_schema, # "action_name": action_name,
}) # "schema": info_with_schema,
# })
except Exception as e: except Exception as e:
self.lab_logger().error(f"[Host Node] Failed to create ActionClient for {action_id}: {str(e)}") self.lab_logger().error(f"[Host Node] Failed to create ActionClient for {action_id}: {str(e)}")
def add_resource_from_outer(self, resources: list["Resource"], device_ids: list[str], bind_parent_ids: list[str], bind_locations: list[Point], other_calling_params: list[str]):
for resource, device_id, bind_parent_id, bind_location, other_calling_param in zip(resources, device_ids, bind_parent_ids, bind_locations, other_calling_params):
# 这里要求device_id传入必须是edge_device_id
namespace = "/devices/" + device_id
srv_address = f"/srv{namespace}/append_resource"
sclient = self.create_client(SerialCommand, srv_address)
sclient.wait_for_service()
request = SerialCommand.Request()
request.command = json.dumps({
"resource": resource,
"namespace": namespace,
"edge_device_id": device_id,
"bind_parent_id": bind_parent_id,
"bind_location": {
"x": bind_location.x,
"y": bind_location.y,
"z": bind_location.z,
},
"other_calling_param": json.loads(other_calling_param) if other_calling_param else {},
}, ensure_ascii=False)
response = sclient.call(request)
pass
pass
def initialize_device(self, device_id: str, device_config: Dict[str, Any]) -> None: def initialize_device(self, device_id: str, device_config: Dict[str, Any]) -> None:
""" """
根据配置初始化设备, 根据配置初始化设备,
@@ -297,13 +320,14 @@ class HostNode(BaseROS2DeviceNode):
action_type = action_value_mapping["type"] action_type = action_value_mapping["type"]
self._action_clients[action_id] = ActionClient(self, action_type, action_id) self._action_clients[action_id] = ActionClient(self, action_type, action_id)
self.lab_logger().debug(f"[Host Node] Created ActionClient (Local): {action_id}") # 子设备再创建用的是Discover发现的 self.lab_logger().debug(f"[Host Node] Created ActionClient (Local): {action_id}") # 子设备再创建用的是Discover发现的
from unilabos.app.mq import mqtt_client # from unilabos.app.mq import mqtt_client
info_with_schema = ros_action_to_json_schema(action_type) # info_with_schema = ros_action_to_json_schema(action_type)
mqtt_client.publish_actions(action_name, { # mqtt_client.publish_actions(action_name, {
"device_id": device_id, # "device_id": device_id,
"action_name": action_name, # "device_type": device_config["class"],
"schema": info_with_schema, # "action_name": action_name,
}) # "schema": info_with_schema,
# })
else: else:
self.lab_logger().warning(f"[Host Node] ActionClient {action_id} already exists.") self.lab_logger().warning(f"[Host Node] ActionClient {action_id} already exists.")
device_key = f"{self.devices_names[device_id]}/{device_id}" # 这里不涉及二级device_id device_key = f"{self.devices_names[device_id]}/{device_id}" # 这里不涉及二级device_id
@@ -619,7 +643,8 @@ class HostNode(BaseROS2DeviceNode):
self.lab_logger().debug(f"[Host Node-Resource] Retrieved from bridge: {len(r)} resources") self.lab_logger().debug(f"[Host Node-Resource] Retrieved from bridge: {len(r)} resources")
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 = [] 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: else:
# 本地物料服务,根据 id 查询物料 # 本地物料服务,根据 id 查询物料
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]

View File

@@ -0,0 +1,60 @@
import rclpy,json
from rclpy.node import Node
from sensor_msgs.msg import JointState
from std_msgs.msg import String
from rclpy.callback_groups import ReentrantCallbackGroup
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
class JointRepublisher(BaseROS2DeviceNode):
def __init__(self,device_id,resource_tracker):
super().__init__(
driver_instance=self,
device_id=device_id,
status_types={},
action_value_mappings={},
hardware_interface={},
print_publish=False,
resource_tracker=resource_tracker,
)
# print('-'*20,device_id)
self.joint_repub = self.create_publisher(String,f'joint_state_repub',10)
# 创建订阅者
self.create_subscription(
JointState,
'/joint_states',
self.listener_callback,
10,
callback_group=ReentrantCallbackGroup()
)
self.msg = String()
def listener_callback(self, msg:JointState):
try:
json_dict = {}
json_dict["name"] = list(msg.name)
json_dict["position"] = list(msg.position)
json_dict["velocity"] = list(msg.velocity)
json_dict["effort"] = list(msg.effort)
self.msg.data = str(json_dict)
self.joint_repub.publish(self.msg)
# print('-'*20)
# print(self.msg.data)
except Exception as e:
print(e)
def main():
rclpy.init()
subscriber = JointRepublisher()
rclpy.spin(subscriber)
subscriber.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
from unilabos.utils.log import logger from unilabos.utils.log import logger
class DeviceNodeResourceTracker: class DeviceNodeResourceTracker(object):
def __init__(self): def __init__(self):
self.resources = [] self.resources = []
@@ -15,43 +15,46 @@ class DeviceNodeResourceTracker:
return resource return resource
def add_resource(self, resource): def add_resource(self, resource):
# 使用内存地址跟踪是否为同一个resource
for r in self.resources: for r in self.resources:
if id(r) == id(resource): if id(r) == id(resource):
return return
# 添加资源到跟踪器
self.resources.append(resource) self.resources.append(resource)
def clear_resource(self): def clear_resource(self):
self.resources = [] self.resources = []
def figure_resource(self, resource): def figure_resource(self, query_resource):
# 使用内存地址跟踪是否为同一个resource if isinstance(query_resource, list):
if isinstance(resource, list): return [self.figure_resource(r) for r in query_resource]
return [self.figure_resource(r) for r in resource] res_id = query_resource.id if hasattr(query_resource, "id") else (query_resource.get("id") if isinstance(query_resource, dict) else None)
res_id = resource.id if hasattr(resource, "id") else None res_name = query_resource.name if hasattr(query_resource, "name") else (query_resource.get("name") if isinstance(query_resource, dict) else None)
res_name = resource.name if hasattr(resource, "name") else None
res_identifier = res_id if res_id else res_name res_identifier = res_id if res_id else res_name
identifier_key = "id" if res_id else "name" identifier_key = "id" if res_id else "name"
resource_cls_type = type(resource) resource_cls_type = type(query_resource)
if res_identifier is None: if res_identifier is None:
logger.warning(f"resource {resource} 没有id或name暂不能对应figure") logger.warning(f"resource {query_resource} 没有id或name暂不能对应figure")
res_list = [] res_list = []
for r in self.resources: for r in self.resources:
res_list.extend( if isinstance(query_resource, dict):
self.loop_find_resource(r, resource_cls_type, identifier_key, getattr(resource, identifier_key)) res_list.extend(
) self.loop_find_resource(r, resource_cls_type, identifier_key, query_resource[identifier_key])
assert len(res_list) == 1, f"找到多个资源,请检查资源是否唯一: {res_list}" )
self.root_resource2resource[id(resource)] = res_list[0] else:
res_list.extend(
self.loop_find_resource(r, resource_cls_type, identifier_key, getattr(query_resource, identifier_key))
)
assert len(res_list) == 1, f"{query_resource} 找到多个资源,请检查资源是否唯一: {res_list}"
self.root_resource2resource[id(query_resource)] = res_list[0]
# 后续加入其他对比方式 # 后续加入其他对比方式
return res_list[0] return res_list[0]
def loop_find_resource(self, resource, resource_cls_type, identifier_key, compare_value): def loop_find_resource(self, resource, target_resource_cls_type, identifier_key, compare_value):
res_list = [] res_list = []
# print(resource, target_resource_cls_type, identifier_key, compare_value)
children = getattr(resource, "children", []) children = getattr(resource, "children", [])
for child in children: for child in children:
res_list.extend(self.loop_find_resource(child, resource_cls_type, identifier_key, compare_value)) res_list.extend(self.loop_find_resource(child, target_resource_cls_type, identifier_key, compare_value))
if resource_cls_type == type(resource): if target_resource_cls_type == type(resource) or target_resource_cls_type == dict:
if hasattr(resource, identifier_key): if hasattr(resource, identifier_key):
if getattr(resource, identifier_key) == compare_value: if getattr(resource, identifier_key) == compare_value:
res_list.append(resource) res_list.append(resource)

View File

@@ -6,6 +6,7 @@
""" """
import asyncio import asyncio
import inspect import inspect
import json
import traceback import traceback
from abc import abstractmethod from abc import abstractmethod
from typing import Type, Any, Dict, Optional, TypeVar, Generic from typing import Type, Any, Dict, Optional, TypeVar, Generic
@@ -218,12 +219,22 @@ class PyLabRobotCreator(DeviceClassCreator[T]):
logger.error(f"PyLabRobot反序列化失败: {deserialize_error}") logger.error(f"PyLabRobot反序列化失败: {deserialize_error}")
logger.error(f"PyLabRobot反序列化堆栈: {stack}") logger.error(f"PyLabRobot反序列化堆栈: {stack}")
return self.device_instance return self.device_instance
def post_create(self): def post_create(self):
if hasattr(self.device_instance, "setup") and asyncio.iscoroutinefunction(getattr(self.device_instance, "setup")): if hasattr(self.device_instance, "setup") and asyncio.iscoroutinefunction(getattr(self.device_instance, "setup")):
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode from unilabos.ros.nodes.base_device_node import ROS2DeviceNode
ROS2DeviceNode.run_async_func(getattr(self.device_instance, "setup")).add_done_callback(lambda x: logger.debug(f"PyLabRobot设备实例 {self.device_instance} 设置完成")) def done_cb(*args):
logger.debug(f"PyLabRobot设备实例 {self.device_instance} 设置完成")
from unilabos.config.config import BasicConfig
if BasicConfig.vis_2d_enable:
from pylabrobot.visualizer.visualizer import Visualizer
vis = Visualizer(resource=self.device_instance, open_browser=True)
def vis_done_cb(*args):
logger.info(f"PyLabRobot设备实例开启了Visualizer {self.device_instance}")
ROS2DeviceNode.run_async_func(vis.setup).add_done_callback(vis_done_cb)
logger.debug(f"PyLabRobot设备实例提交开启Visualizer {self.device_instance}")
ROS2DeviceNode.run_async_func(getattr(self.device_instance, "setup")).add_done_callback(done_cb)
class ProtocolNodeCreator(DeviceClassCreator[T]): class ProtocolNodeCreator(DeviceClassCreator[T]):

View File

@@ -43,6 +43,25 @@ set(action_files
"action/LiquidHandlerStamp.action" "action/LiquidHandlerStamp.action"
"action/LiquidHandlerTransfer.action" "action/LiquidHandlerTransfer.action"
"action/DPLiquidHandlerAddLiquid.action"
"action/DPLiquidHandlerCustomDelay.action"
"action/DPLiquidHandlerMix.action"
"action/DPLiquidHandlerMoveTo.action"
"action/DPLiquidHandlerRemoveLiquid.action"
"action/DPLiquidHandlerSetTiprack.action"
"action/DPLiquidHandlerTouchTip.action"
"action/DPLiquidHandlerTransferLiquid.action"
"action/EmptyIn.action"
"action/FloatSingleInput.action"
"action/IntSingleInput.action"
"action/StrSingleInput.action"
"action/Point3DSeparateInput.action"
"action/ResourceCreateFromOuter.action"
"action/SolidDispenseAddPowderTube.action"
"action/PumpTransfer.action" "action/PumpTransfer.action"
"action/Clean.action" "action/Clean.action"
"action/Separate.action" "action/Separate.action"

View File

@@ -0,0 +1,20 @@
float64[] asp_vols
float64[] dis_vols
Resource[] reagent_sources
Resource[] targets
int32[] use_channels
float64[] flow_rates
geometry_msgs/Point[] offsets
float64[] liquid_height
float64[] blow_out_air_volume
string spread
bool is_96_well
int32 mix_time
int32 mix_vol
int32 mix_rate
float64 mix_liquid_height
string[] none_keys
---
bool success
---
# 反馈

View File

@@ -0,0 +1,6 @@
float64 seconds
string msg
---
bool success
---
# 反馈

View File

@@ -0,0 +1,11 @@
Resource[] targets
int32 mix_time
int32 mix_vol
float64 height_to_bottom
geometry_msgs/Point[] offsets
float64 mix_rate
string[] none_keys
---
bool success
---
# 反馈

View File

@@ -0,0 +1,7 @@
Resource well
float64 dis_to_top
int32 channel
---
bool success
---
# 反馈

View File

@@ -0,0 +1,17 @@
float64[] vols
Resource[] sources
Resource waste_liquid
int32[] use_channels
float64[] flow_rates
geometry_msgs/Point[] offsets
float64[] liquid_height
float64[] blow_out_air_volume
string spread
int32[] delays
bool is_96_well
float64[] top
string[] none_keys
---
bool success
---
# 反馈

View File

@@ -0,0 +1,5 @@
Resource[] tip_racks
---
bool success
---
# 反馈

View File

@@ -0,0 +1,5 @@
Resource[] targets
---
bool success
---
# 反馈

View File

@@ -0,0 +1,25 @@
float64[] asp_vols
float64[] dis_vols
Resource[] sources
Resource[] targets
Resource[] tip_racks
int32[] use_channels
float64[] asp_flow_rates
float64[] dis_flow_rates
geometry_msgs/Point[] offsets
bool touch_tip
float64[] liquid_height
float64[] blow_out_air_volume
string spread
bool is_96_well
string mix_stage
int32[] mix_times
int32 mix_vol
int32 mix_rate
float64 mix_liquid_height
int32[] delays
string[] none_keys
---
bool success
---
# 反馈

View File

@@ -1,12 +1,11 @@
# Bio
Resource[] resources Resource[] resources
float64[] vols float64[] vols
int32[] use_channels int32[] use_channels
float64[] flow_rates float64[] flow_rates
float64 end_delay
geometry_msgs/Point[] offsets geometry_msgs/Point[] offsets
float64[] liquid_height float64[] liquid_height
float64[] blow_out_air_volume float64[] blow_out_air_volume
string spread="wide"
--- ---
bool success bool success
--- ---

View File

@@ -1,4 +1,3 @@
# Bio
# 请求字段 # 请求字段
Resource[] resources Resource[] resources
float64[] vols float64[] vols
@@ -6,7 +5,7 @@ int32[] use_channels
float64[] flow_rates float64[] flow_rates
geometry_msgs/Point[] offsets geometry_msgs/Point[] offsets
int32[] blow_out_air_volume int32[] blow_out_air_volume
string spread string spread="wide"
--- ---
# 结果字段 # 结果字段
bool success bool success

View File

@@ -0,0 +1,8 @@
Resource[] resources
string[] device_ids
string[] bind_parent_ids
geometry_msgs/Point[] bind_locations
string[] other_calling_params
---
bool success
---