Compare commits

...

20 Commits

Author SHA1 Message Date
Xuwznln
48895a9bb1 Update repo files. 2025-11-15 03:15:44 +08:00
Xuwznln
891f126ed6 bump version to 0.10.10 2025-11-15 03:11:37 +08:00
Xuwznln
4d3475a849 Update devices 2025-11-15 03:11:36 +08:00
WenzheG
b475db66df nmr 2025-11-15 03:11:35 +08:00
ZiWei
a625a86e3e HR物料同步,前端展示位置修复 (#135)
* 更新Bioyond工作站配置,添加新的物料类型映射和载架定义,优化物料查询逻辑

* 添加Bioyond实验配置文件,定义物料类型映射和设备配置

* 更新bioyond_warehouse_reagent_stack方法,修正试剂堆栈尺寸和布局描述

* 更新Bioyond实验配置,修正物料类型映射,优化设备配置

* 更新Bioyond资源同步逻辑,优化物料入库流程,增强错误处理和日志记录

* 更新Bioyond资源,添加配液站和反应站专用载架,优化仓库工厂函数的排序方式

* 更新Bioyond资源,添加配液站和反应站相关载架,优化试剂瓶和样品瓶配置

* 更新Bioyond实验配置,修正试剂瓶载架ID,确保与设备匹配

* 更新Bioyond资源,移除反应站单烧杯载架,添加反应站单烧瓶载架分类

* Refactor Bioyond resource synchronization and update bottle carrier definitions

- Removed traceback printing in error handling for Bioyond synchronization.
- Enhanced logging for existing Bioyond material ID usage during synchronization.
- Added new bottle carrier definitions for single flask and updated existing ones.
- Refactored dispensing station and reaction station bottle definitions for clarity and consistency.
- Improved resource mapping and error handling in graphio for Bioyond resource conversion.
- Introduced layout parameter in warehouse factory for better warehouse configuration.

* 更新Bioyond仓库工厂,添加排序方式支持,优化坐标计算逻辑

* 更新Bioyond载架和甲板配置,调整样品板尺寸和仓库坐标

* 更新Bioyond资源同步,增强占用位置日志信息,修正坐标转换逻辑

* 更新Bioyond反应站和分配站配置,调整材料类型映射和ID,移除不必要的项

* support name change during materials change

* fix json dumps

* correct tip

* 优化调度器API路径,更新相关方法描述

* 更新 BIOYOND 载架相关文档,调整 API 以支持自带试剂瓶的载架类型,修复资源获取时的子物料处理逻辑

* 实现资源删除时的同步处理,优化出库操作逻辑

* 修复 ItemizedCarrier 中的可见性逻辑

* 保存 Bioyond 原始信息到 unilabos_extra,以便出库时查询

* 根据 resource.capacity 判断是试剂瓶(载架)还是多瓶载架,走不同的奔曜转换

* Fix bioyond bottle_carriers ordering

* 优化 Bioyond 物料同步逻辑,增强坐标解析和位置更新处理

* disable slave connect websocket

* correct remove_resource stats

* change uuid logger to trace level

* enable slave mode

* refactor(bioyond): 统一资源命名并优化物料同步逻辑

- 将DispensingStation和ReactionStation资源统一为PolymerStation命名
- 优化物料同步逻辑,支持耗材类型(typeMode=0)的查询
- 添加物料默认参数配置功能
- 调整仓库坐标布局
- 清理废弃资源定义

* feat(warehouses): 为仓库函数添加col_offset和layout参数

* refactor: 更新实验配置中的物料类型映射命名

将DispensingStation和ReactionStation的物料类型映射统一更名为PolymerStation,保持命名一致性

* fix: 更新实验配置中的载体名称从6VialCarrier到6StockCarrier

* feat(bioyond): 实现物料创建与入库分离逻辑

将物料同步流程拆分为两个独立阶段:transfer阶段只创建物料,add阶段执行入库
简化状态检查接口,仅返回连接状态

* fix(reaction_station): 修正液体进料烧杯体积单位并增强返回结果

将液体进料烧杯的体积单位从μL改为g以匹配实际使用场景
在返回结果中添加merged_workflow和order_params字段,提供更完整的工作流信息

* feat(dispensing_station): 在任务创建返回结果中添加order_params信息

在create_order方法返回结果中增加order_params字段,以便调用方获取完整的任务参数

* fix(dispensing_station): 修改90%物料分配逻辑从分成3份改为直接使用

原逻辑将主称固体平均分成3份作为90%物料,现改为直接使用main_portion

* feat(bioyond): 添加任务编码和任务ID的输出,支持批量任务创建后的状态监控

* refactor(registry): 简化设备配置中的任务结果处理逻辑

将多个单独的任务编码和ID字段合并为统一的return_info字段
更新相关描述以反映新的数据结构

* feat(工作站): 添加HTTP报送服务和任务完成状态跟踪

- 在graphio.py中添加API必需字段
- 实现工作站HTTP服务启动和停止逻辑
- 添加任务完成状态跟踪字典和等待方法
- 重写任务完成报送处理方法记录状态
- 支持批量任务完成等待和报告获取

* refactor(dispensing_station): 移除wait_for_order_completion_and_get_report功能

该功能已被wait_for_multiple_orders_and_get_reports替代,简化代码结构

* fix: 更新任务报告API错误

* fix(workstation_http_service): 修复状态查询中device_id获取逻辑

处理状态查询时安全获取device_id,避免因属性不存在导致的异常

* fix(bioyond_studio): 改进物料入库失败时的错误处理和日志记录

在物料入库API调用失败时,添加更详细的错误信息打印
同时修正station.py中对空响应和失败情况的判断逻辑

* refactor(bioyond): 优化瓶架载体的分配逻辑和注释说明

重构瓶架载体的分配逻辑,使用嵌套循环替代硬编码索引分配
添加更详细的坐标映射说明,明确PLR与Bioyond坐标的对应关系

* fix(bioyond_rpc): 修复物料入库成功时无data字段返回空的问题

当API返回成功但无data字段时,返回包含success标识的字典而非空字典

---------

Co-authored-by: Xuwznln <18435084+Xuwznln@users.noreply.github.com>
Co-authored-by: Junhan Chang <changjh@dp.tech>
2025-11-15 03:11:34 +08:00
xyc
37e0f1037c add new laiyu liquid driver, yaml and json files (#164) 2025-11-15 03:11:33 +08:00
tt
a242253145 标准化opcua设备接入unilab (#78)
* 初始提交,只保留工作区当前状态

* remove redundant arm_slider meshes

---------

Co-authored-by: Junhan Chang <changjh@dp.tech>
2025-11-15 03:11:31 +08:00
q434343
448e0074b7 3d sim (#97)
* 修改lh的json启动

* 修改lh的json启动

* 修改backend,做成sim的通用backend

* 修改yaml的地址,3D模型适配网页生产环境

* 添加laiyu硬件连接

* 修改移液枪的状态判断方法,

修改移液枪的状态判断方法,
添加三轴的表定点与零点之间的转换
添加三轴真实移动的backend

* 修改laiyu移液站

简化移动方法,
取消软件限制位置,
修改当值使用Z轴时也需要重新复位Z轴的问题

* 更新lh以及laiyu workshop

1,现在可以直接通过修改backend,适配其他的移液站,主类依旧使用LiquidHandler,不用重新编写

2,修改枪头判断标准,使用枪头自身判断而不是类的判断,

3,将归零参数用毫米计算,方便手动调整,

4,修改归零方式,上电使用机械归零,确定机械零点,手动归零设置工作区域零点方便计算,二者互不干涉

* 修改枪头动作

* 修改虚拟仿真方法

---------

Co-authored-by: zhangshixiang <@zhangshixiang>
Co-authored-by: Junhan Chang <changjh@dp.tech>
2025-11-15 03:11:30 +08:00
lixinyu1011
304827fc8d 1114物料手册定义教程byxinyu (#165)
* 宜宾奔耀工站deck前端by_Xinyu

* 构建物料教程byxinyu

* 1114物料手册定义教程
2025-11-15 03:11:29 +08:00
Harry Liu
872b3d781f PRCXI Reset Error Correction (#166)
* change 9320 desk row number to 4

* Updated 9320 host address

* Updated 9320 host address

* Add **kwargs in classes: PRCXI9300Deck and PRCXI9300Container

* Removed all sample_id in prcxi_9320.json to avoid KeyError

* 9320 machine testing settings

* Typo

* Rewrite setup logic to clear error code

* 初始化 step_mode 属性
2025-11-15 03:11:29 +08:00
Xuwznln
813400f2b4 bump version to 0.10.9
update registry
2025-11-15 02:45:30 +08:00
Xuwznln
b6dfe2b944 Resource update & asyncio fix
correct bioyond config

prcxi example

fix append_resource

fix regularcontainer

fix cancel error

fix resource_get param

fix json dumps

support name change during materials change

enable slave mode

change uuid logger to trace level

correct remove_resource stats

disable slave connect websocket

adjust with_children param

modify devices to use correct executor (sleep, create_task)

support sleep and create_task in node

fix run async execution error
2025-11-15 02:45:12 +08:00
WenzheG
8807865649 添加Raman和xrd相关代码 2025-11-15 02:44:03 +08:00
Guangxin Zhang
5fc7eb7586 封膜仪、撕膜仪、耗材站接口 2025-11-15 02:44:02 +08:00
ZiWei
9bd72b48e1 Update workstation.
modify workstation_architecture docs

bioyond_HR (#133)

* feat: Enhance Bioyond synchronization and resource management

- Implemented synchronization for all material types (consumables, samples, reagents) from Bioyond, logging detailed information for each type.
- Improved error handling and logging during synchronization processes.
- Added functionality to save Bioyond material IDs in UniLab resources for future updates.
- Enhanced the `sync_to_external` method to handle material movements correctly, including querying and creating materials in Bioyond.
- Updated warehouse configurations to support new storage types and improved layout for better resource management.
- Introduced new resource types such as reactors and tip boxes, with detailed specifications.
- Modified warehouse factory to support column offsets for naming conventions (e.g., A05-D08).
- Improved resource tracking by merging extra attributes instead of overwriting them.
- Added a new method for updating resources in Bioyond, ensuring better synchronization of resource changes.

* feat: 添加TipBox和Reactor的配置到bottles.yaml

* fix: 修复液体投料方法中的volume参数处理逻辑

修复solid_feeding_vials方法中的volume参数处理逻辑,优化solvents参数的使用条件

更新液体投料方法,支持通过溶剂信息自动计算体积,添加solvents参数并更新文档描述

Add batch creation methods for vial and solution tasks

添加批量创建90%10%小瓶投料任务和二胺溶液配置任务的功能,更新相关参数和默认值
2025-11-15 02:43:50 +08:00
Xuwznln
42b78ab4c1 Update resource extra & uuid.
use ordering to convert identifier to idx

convert identifier to site idx

correct extra key

update extra before transfer

fix multiple instance error

add resource_tree_transfer func

fox itemrized carrier assign child resource

support internal device material transfer

remove extra key

use same callback group

support material extra

support material extra
support update_resource_site in extra
2025-11-15 02:43:13 +08:00
Xianwei Qi
9645609a05 PRCXI Update
修改prcxi连线

prcxi样例图

Create example_prcxi.json
2025-11-15 02:41:30 +08:00
ZiWei
a2a827d7ac Update workstation & bioyond example
Refine descriptions in Bioyond reaction station YAML

Updated and clarified field and operation descriptions in the reaction_station_bioyond.yaml file for improved accuracy and consistency. Changes include more precise terminology, clearer parameter explanations, and standardized formatting for operation schemas.

refactor(workstation): 更新反应站参数描述并添加分液站配置文件

修正反应站方法参数描述,使其更准确清晰
添加bioyond_dispensing_station.yaml配置文件

add create_workflow script and test

add invisible_slots to carriers

fix(warehouses): 修正bioyond_warehouse_1x4x4仓库的尺寸参数

调整仓库的num_items_x和num_items_z值以匹配实际布局,并更新物品尺寸参数

save resource get data. allow empty value for layout and cross_section_type

More decks&plates support for bioyond (#115)

refactor(registry): 重构反应站设备配置,简化并更新操作命令

移除旧的自动操作命令,新增针对具体化学操作的命令配置
更新模块路径和配置结构,优化参数定义和描述

fix(dispensing_station): 修正物料信息查询方法调用

将直接调用material_id_query改为通过hardware_interface调用,以符合接口设计规范
2025-11-15 02:40:54 +08:00
ZiWei
bb3ca645a4 Update graphio together with workstation design.
fix(reaction_station): 为步骤参数添加Value字段传个BY后端

fix(bioyond/warehouses): 修正仓库尺寸和物品排列参数

调整仓库的x轴和z轴物品数量以及物品尺寸参数,使其符合4x1x4的规格要求

fix warehouse serialize/deserialize

fix bioyond converter

fix itemized_carrier.unassign_child_resource

allow not-loaded MSG in registry

add layout serializer & converter

warehouseuse A1-D4; add warehouse layout

fix(graphio): 修正bioyond到plr资源转换中的坐标计算错误

Fix resource assignment and type mapping issues

Corrects resource assignment in ItemizedCarrier by using the correct spot key from _ordering. Updates graphio to use 'typeName' instead of 'name' for type mapping in resource_bioyond_to_plr. Renames DummyWorkstation to BioyondWorkstation in workstation_http_service for clarity.
2025-11-15 02:39:01 +08:00
Junhan Chang
37ee43d19a Update ResourceTracker
add more enumeration in POSE

fix converter in resource_tracker
2025-11-15 02:38:01 +08:00
214 changed files with 33197 additions and 5067 deletions

View File

@@ -1,6 +1,6 @@
package:
name: unilabos
version: 0.10.7
version: 0.10.10
source:
path: ../unilabos

View File

@@ -1,15 +1,18 @@
156 Xuwznln <18435084+Xuwznln@users.noreply.github.com>
39 Junhan Chang <changjh@dp.tech>
9 wznln <18435084+Xuwznln@users.noreply.github.com>
8 Guangxin Zhang <guangxin.zhang.bio@gmail.com>
56 Xuwznln <18435084+Xuwznln@users.noreply.github.com>
10 wznln <18435084+Xuwznln@users.noreply.github.com>
6 Junhan Chang <changjh@dp.tech>
5 ZiWei <131428629+ZiWei09@users.noreply.github.com>
2 Guangxin Zhang <guangxin.zhang.bio@gmail.com>
2 Junhan Chang <changjh@pku.edu.cn>
2 Xie Qiming <97236197+Andy6M@users.noreply.github.com>
2 WenzheG <wenzheguo32@gmail.com>
1 Harry Liu <113173203+ALITTLELZ@users.noreply.github.com>
1 Harvey Que <103566763+Mile-Away@users.noreply.github.com>
1 Junhan Chang <1700011741@pku.edu.cn>
1 LccLink <1951855008@qq.com>
1 h840473807 <47357934+h840473807@users.noreply.github.com>
1 Xianwei Qi <qxw@stu.pku.edu.cn>
1 hh.(SII) <103566763+Mile-Away@users.noreply.github.com>
1 lixinyu1011 <61094742+lixinyu1011@users.noreply.github.com>
1 shiyubo0410 <shiyubo@dp.tech>
1 q434343 <73513873+q434343@users.noreply.github.com>
1 tt <166512503+tt11142023@users.noreply.github.com>
1 xyc <49015816+xiaoyu10031@users.noreply.github.com>
1 王俊杰 <1800011822@pku.edu.cn>
1 王俊杰 <43375851+wjjxxx@users.noreply.github.com>

View File

@@ -24,6 +24,7 @@ extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.napoleon", # 如果您使用 Google 或 NumPy 风格的 docstrings
"sphinx_rtd_theme",
"sphinxcontrib.mermaid"
]
source_suffix = {
@@ -42,6 +43,8 @@ myst_enable_extensions = [
"substitution",
]
myst_fence_as_directive = ["mermaid"]
templates_path = ["_templates"]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
@@ -203,3 +206,5 @@ def generate_action_includes(app):
def setup(app):
app.connect("builder-inited", generate_action_includes)
app.add_js_file("https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js")
app.add_js_file(None, body="mermaid.initialize({startOnLoad:true});")

View File

@@ -1,88 +1,26 @@
## 简单单变量动作函数
### `SendCmd`
```{literalinclude} ../../unilabos_msgs/action/SendCmd.action
:language: yaml
```
---
### `StrSingleInput`
```{literalinclude} ../../unilabos_msgs/action/StrSingleInput.action
:language: yaml
```
---
### `IntSingleInput`
```{literalinclude} ../../unilabos_msgs/action/IntSingleInput.action
:language: yaml
```
---
### `FloatSingleInput`
```{literalinclude} ../../unilabos_msgs/action/FloatSingleInput.action
:language: yaml
```
---
### `Point3DSeparateInput`
```{literalinclude} ../../unilabos_msgs/action/Point3DSeparateInput.action
:language: yaml
```
---
### `Wait`
```{literalinclude} ../../unilabos_msgs/action/Wait.action
:language: yaml
```
---
----
## 常量有机化学操作
Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab.io/chemputer/xdl/standard/full_steps_specification.html#),包含有机合成实验中常见的操作,如加热、搅拌、冷却等。
### `Clean`
```{literalinclude} ../../unilabos_msgs/action/Clean.action
:language: yaml
```
---
### `EvacuateAndRefill`
```{literalinclude} ../../unilabos_msgs/action/EvacuateAndRefill.action
:language: yaml
```
---
### `Evaporate`
```{literalinclude} ../../unilabos_msgs/action/Evaporate.action
:language: yaml
```
---
### `HeatChill`
```{literalinclude} ../../unilabos_msgs/action/HeatChill.action
:language: yaml
```
---
----
### `HeatChillStart`
@@ -90,7 +28,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
:language: yaml
```
---
----
### `HeatChillStop`
@@ -98,7 +36,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
:language: yaml
```
---
----
### `PumpTransfer`
@@ -106,195 +44,12 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
:language: yaml
```
---
### `Separate`
```{literalinclude} ../../unilabos_msgs/action/Separate.action
:language: yaml
```
---
### `Stir`
```{literalinclude} ../../unilabos_msgs/action/Stir.action
:language: yaml
```
---
### `Add`
```{literalinclude} ../../unilabos_msgs/action/Add.action
:language: yaml
```
---
### `AddSolid`
```{literalinclude} ../../unilabos_msgs/action/AddSolid.action
:language: yaml
```
---
### `AdjustPH`
```{literalinclude} ../../unilabos_msgs/action/AdjustPH.action
:language: yaml
```
---
### `Centrifuge`
```{literalinclude} ../../unilabos_msgs/action/Centrifuge.action
:language: yaml
```
---
### `CleanVessel`
```{literalinclude} ../../unilabos_msgs/action/CleanVessel.action
:language: yaml
```
---
### `Crystallize`
```{literalinclude} ../../unilabos_msgs/action/Crystallize.action
:language: yaml
```
---
### `Dissolve`
```{literalinclude} ../../unilabos_msgs/action/Dissolve.action
:language: yaml
```
---
### `Dry`
```{literalinclude} ../../unilabos_msgs/action/Dry.action
:language: yaml
```
---
### `Filter`
```{literalinclude} ../../unilabos_msgs/action/Filter.action
:language: yaml
```
---
### `FilterThrough`
```{literalinclude} ../../unilabos_msgs/action/FilterThrough.action
:language: yaml
```
---
### `Hydrogenate`
```{literalinclude} ../../unilabos_msgs/action/Hydrogenate.action
:language: yaml
```
---
### `Purge`
```{literalinclude} ../../unilabos_msgs/action/Purge.action
:language: yaml
```
---
### `Recrystallize`
```{literalinclude} ../../unilabos_msgs/action/Recrystallize.action
:language: yaml
```
---
### `RunColumn`
```{literalinclude} ../../unilabos_msgs/action/RunColumn.action
:language: yaml
```
---
### `StartPurge`
```{literalinclude} ../../unilabos_msgs/action/StartPurge.action
:language: yaml
```
---
### `StartStir`
```{literalinclude} ../../unilabos_msgs/action/StartStir.action
:language: yaml
```
---
### `StopPurge`
```{literalinclude} ../../unilabos_msgs/action/StopPurge.action
:language: yaml
```
---
### `StopStir`
```{literalinclude} ../../unilabos_msgs/action/StopStir.action
:language: yaml
```
---
### `Transfer`
```{literalinclude} ../../unilabos_msgs/action/Transfer.action
:language: yaml
```
---
### `WashSolid`
```{literalinclude} ../../unilabos_msgs/action/WashSolid.action
:language: yaml
```
---
----
## 移液工作站及相关生物自动化设备操作
Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.org/user_guide/index.html),包含生物实验中常见的操作,如移液、混匀、离心等。
### `LiquidHandlerAspirate`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerAspirate.action
:language: yaml
```
---
### `LiquidHandlerDiscardTips`
@@ -302,15 +57,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
---
### `LiquidHandlerDispense`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerDispense.action
:language: yaml
```
---
----
### `LiquidHandlerDropTips`
@@ -318,7 +65,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
---
----
### `LiquidHandlerDropTips96`
@@ -326,7 +73,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
---
----
### `LiquidHandlerMoveLid`
@@ -334,7 +81,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
---
----
### `LiquidHandlerMovePlate`
@@ -342,7 +89,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
---
----
### `LiquidHandlerMoveResource`
@@ -350,7 +97,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
---
----
### `LiquidHandlerPickUpTips`
@@ -358,7 +105,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
---
----
### `LiquidHandlerPickUpTips96`
@@ -366,7 +113,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
---
----
### `LiquidHandlerReturnTips`
@@ -374,7 +121,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
---
----
### `LiquidHandlerReturnTips96`
@@ -382,7 +129,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
---
----
### `LiquidHandlerStamp`
@@ -390,129 +137,17 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
---
### `LiquidHandlerTransfer`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerTransfer.action
:language: yaml
```
---
### `LiquidHandlerAdd`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerAdd.action
:language: yaml
```
---
### `LiquidHandlerIncubateBiomek`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerIncubateBiomek.action
:language: yaml
```
---
### `LiquidHandlerMix`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerMix.action
:language: yaml
```
---
### `LiquidHandlerMoveBiomek`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerMoveBiomek.action
:language: yaml
```
---
### `LiquidHandlerMoveTo`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerMoveTo.action
:language: yaml
```
---
### `LiquidHandlerOscillateBiomek`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerOscillateBiomek.action
:language: yaml
```
---
### `LiquidHandlerProtocolCreation`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerProtocolCreation.action
:language: yaml
```
---
### `LiquidHandlerRemove`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerRemove.action
:language: yaml
```
---
### `LiquidHandlerSetGroup`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerSetGroup.action
:language: yaml
```
---
### `LiquidHandlerSetLiquid`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerSetLiquid.action
:language: yaml
```
---
### `LiquidHandlerSetTipRack`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerSetTipRack.action
:language: yaml
```
---
### `LiquidHandlerTransferBiomek`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerTransferBiomek.action
:language: yaml
```
---
### `LiquidHandlerTransferGroup`
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerTransferGroup.action
:language: yaml
```
---
----
## 多工作站及小车运行、物料转移
### `AGVTransfer`
```{literalinclude} ../../unilabos_msgs/action/AGVTransfer.action
:language: yaml
```
---
----
### `WorkStationRun`
@@ -520,64 +155,12 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
:language: yaml
```
---
### `ResetHandling`
```{literalinclude} ../../unilabos_msgs/action/ResetHandling.action
:language: yaml
```
---
### `ResourceCreateFromOuter`
```{literalinclude} ../../unilabos_msgs/action/ResourceCreateFromOuter.action
:language: yaml
```
---
### `ResourceCreateFromOuterEasy`
```{literalinclude} ../../unilabos_msgs/action/ResourceCreateFromOuterEasy.action
:language: yaml
```
---
### `SetPumpPosition`
```{literalinclude} ../../unilabos_msgs/action/SetPumpPosition.action
:language: yaml
```
---
## 固体分配与处理设备操作
### `SolidDispenseAddPowderTube`
```{literalinclude} ../../unilabos_msgs/action/SolidDispenseAddPowderTube.action
:language: yaml
```
---
## 其他设备操作
### `EmptyIn`
```{literalinclude} ../../unilabos_msgs/action/EmptyIn.action
:language: yaml
```
---
----
## 机械臂、夹爪等机器人设备
Uni-Lab 机械臂、机器人、夹爪和导航指令集沿用 ROS2 的 `control_msgs` 和 `nav2_msgs`
### `FollowJointTrajectory`
```yaml
@@ -645,8 +228,7 @@ trajectory_msgs/MultiDOFJointTrajectoryPoint multi_dof_error
```
---
----
### `GripperCommand`
```yaml
@@ -664,19 +246,42 @@ bool reached_goal # True iff the gripper position has reached the commanded setp
```
---
----
### `JointTrajectory`
```yaml
trajectory_msgs/JointTrajectory trajectory
---
---
```
---
----
### `ParallelGripperCommand`
```yaml
# Parallel grippers refer to an end effector where two opposing fingers grasp an object from opposite sides.
sensor_msgs/JointState command
# name: the name(s) of the joint this command is requesting
# position: desired position of each gripper joint (radians or meters)
# velocity: (optional, not used if empty) max velocity of the joint allowed while moving (radians or meters / second)
# effort: (optional, not used if empty) max effort of the joint allowed while moving (Newtons or Newton-meters)
---
sensor_msgs/JointState state # The current gripper state.
# position of each joint (radians or meters)
# optional: velocity of each joint (radians or meters / second)
# optional: effort of each joint (Newtons or Newton-meters)
bool stalled # True if the gripper is exerting max effort and not moving
bool reached_goal # True if the gripper position has reached the commanded setpoint
---
sensor_msgs/JointState state # The current gripper state.
# position of each joint (radians or meters)
# optional: velocity of each joint (radians or meters / second)
# optional: effort of each joint (Newtons or Newton-meters)
```
----
### `PointHead`
```yaml
@@ -686,13 +291,12 @@ string pointing_frame
builtin_interfaces/Duration min_duration
float64 max_velocity
---
---
float64 pointing_angle_error
```
---
----
### `SingleJointPosition`
```yaml
@@ -700,16 +304,15 @@ float64 position
builtin_interfaces/Duration min_duration
float64 max_velocity
---
---
std_msgs/Header header
float64 position
float64 velocity
float64 error
```
---
----
### `AssistedTeleop`
```yaml
@@ -721,10 +324,10 @@ builtin_interfaces/Duration total_elapsed_time
---
#feedback
builtin_interfaces/Duration current_teleop_duration
```
---
----
### `BackUp`
```yaml
@@ -738,10 +341,10 @@ builtin_interfaces/Duration total_elapsed_time
---
#feedback definition
float32 distance_traveled
```
---
----
### `ComputePathThroughPoses`
```yaml
@@ -756,10 +359,10 @@ nav_msgs/Path path
builtin_interfaces/Duration planning_time
---
#feedback definition
```
---
----
### `ComputePathToPose`
```yaml
@@ -774,10 +377,10 @@ nav_msgs/Path path
builtin_interfaces/Duration planning_time
---
#feedback definition
```
---
----
### `DriveOnHeading`
```yaml
@@ -791,10 +394,10 @@ builtin_interfaces/Duration total_elapsed_time
---
#feedback definition
float32 distance_traveled
```
---
----
### `DummyBehavior`
```yaml
@@ -805,10 +408,10 @@ std_msgs/String command
builtin_interfaces/Duration total_elapsed_time
---
#feedback definition
```
---
----
### `FollowPath`
```yaml
@@ -823,10 +426,10 @@ std_msgs/Empty result
#feedback definition
float32 distance_to_goal
float32 speed
```
---
----
### `FollowWaypoints`
```yaml
@@ -838,10 +441,10 @@ int32[] missed_waypoints
---
#feedback definition
uint32 current_waypoint
```
---
----
### `NavigateThroughPoses`
```yaml
@@ -859,10 +462,10 @@ builtin_interfaces/Duration estimated_time_remaining
int16 number_of_recoveries
float32 distance_remaining
int16 number_of_poses_remaining
```
---
----
### `NavigateToPose`
```yaml
@@ -879,10 +482,10 @@ builtin_interfaces/Duration navigation_time
builtin_interfaces/Duration estimated_time_remaining
int16 number_of_recoveries
float32 distance_remaining
```
---
----
### `SmoothPath`
```yaml
@@ -898,10 +501,10 @@ builtin_interfaces/Duration smoothing_duration
bool was_completed
---
#feedback definition
```
---
----
### `Spin`
```yaml
@@ -914,10 +517,10 @@ builtin_interfaces/Duration total_elapsed_time
---
#feedback definition
float32 angular_distance_traveled
```
---
----
### `Wait`
```yaml
@@ -929,6 +532,7 @@ builtin_interfaces/Duration total_elapsed_time
---
#feedback definition
builtin_interfaces/Duration time_left
```
---
----

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

View File

@@ -0,0 +1,405 @@
# 物料构建指南
## 概述
在UniLab-OS系统中任何工作站中所需要用到的物料主要包括四个核心组件
1. **桌子Deck** - 工作台面,定义整个工作空间的布局
2. **堆栈Warehouse** - 存储区域,用于放置载具和物料
3. **载具Carriers** - 承载瓶子等物料的容器架
4. **瓶子Bottles** - 实际的物料容器
本文档以BioYond工作站为例详细说明如何构建这些物料组件。
## 文件结构
物料定义文件位于 `unilabos/resources/` 文件夹中:
```
unilabos/resources/bioyond/
├── decks.py # 桌子定义
├── YB_warehouses.py # 堆栈定义
├── YB_bottle_carriers.py # 载具定义
└── YB_bottles.py # 瓶子定义
```
对应的注册表文件位于 `unilabos/registry/resources/bioyond/` 文件夹中:
```
unilabos/registry/resources/bioyond/
├── deck.yaml # 桌子注册表
├── YB_bottle_carriers.yaml # 载具注册表
└── YB_bottle.yaml # 瓶子注册表
```
## 1. 桌子Deck构建
桌子是整个工作站的基础,定义了工作空间的尺寸和各个组件的位置。
### 代码示例 (decks.py)
```python
from pylabrobot.resources import Coordinate, Deck
from unilabos.resources.bioyond.YB_warehouses import (
bioyond_warehouse_2x2x1,
bioyond_warehouse_3x5x1,
bioyond_warehouse_20x1x1,
bioyond_warehouse_3x3x1,
bioyond_warehouse_10x1x1
)
class BIOYOND_YB_Deck(Deck):
def __init__(
self,
name: str = "YB_Deck",
size_x: float = 4150, # 桌子X方向尺寸 (mm)
size_y: float = 1400.0, # 桌子Y方向尺寸 (mm)
size_z: float = 2670.0, # 桌子Z方向尺寸 (mm)
category: str = "deck",
setup: bool = False
) -> None:
super().__init__(name=name, size_x=4150.0, size_y=1400.0, size_z=2670.0)
if setup:
self.setup() # 当在工作站配置中setup为True时自动创建并放置所有预定义的堆栈
def setup(self) -> None:
# 定义桌子上的各个仓库区域
self.warehouses = {
"自动堆栈-左": bioyond_warehouse_2x2x1("自动堆栈-左"),
"自动堆栈-右": bioyond_warehouse_2x2x1("自动堆栈-右"),
"手动堆栈-左": bioyond_warehouse_3x5x1("手动堆栈-左"),
"手动堆栈-右": bioyond_warehouse_3x5x1("手动堆栈-右"),
"粉末加样头堆栈": bioyond_warehouse_20x1x1("粉末加样头堆栈"),
"配液站内试剂仓库": bioyond_warehouse_3x3x1("配液站内试剂仓库"),
"试剂替换仓库": bioyond_warehouse_10x1x1("试剂替换仓库"),
}
# 定义各个仓库在桌子上的坐标位置
self.warehouse_locations = {
"自动堆栈-左": Coordinate(-100.3, 171.5, 0.0),
"自动堆栈-右": Coordinate(3960.1, 155.9, 0.0),
"手动堆栈-左": Coordinate(-213.3, 804.4, 0.0),
"手动堆栈-右": Coordinate(3960.1, 807.6, 0.0),
"粉末加样头堆栈": Coordinate(415.0, 1301.0, 0.0),
"配液站内试剂仓库": Coordinate(2162.0, 437.0, 0.0),
"试剂替换仓库": Coordinate(1173.0, 802.0, 0.0),
}
# 将仓库分配到桌子的指定位置
for warehouse_name, warehouse in self.warehouses.items():
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
```
### 在工作站配置中的使用
当在工作站配置文件中定义桌子时,可以通过`setup`参数控制是否自动建立所有堆栈:
```json
{
"id": "YB_Bioyond_Deck",
"name": "YB_Bioyond_Deck",
"children": [],
"parent": "bioyond_cell_workstation",
"type": "deck",
"class": "BIOYOND_YB_Deck",
"config": {
"type": "BIOYOND_YB_Deck",
"setup": true
},
"data": {}
}
```
**重要说明**
-`"setup": true` 时,系统会自动调用桌子的 `setup()` 方法
- 这将创建并放置所有预定义的堆栈到桌子上的指定位置
- 如果 `"setup": false` 或省略该参数,则只创建空桌子,需要手动添加堆栈
### 关键要点注释
- `size_x`, `size_y`, `size_z`: 定义桌子的物理尺寸
- `warehouses`: 字典类型,包含桌子上所有的仓库区域
- `warehouse_locations`: 定义每个仓库在桌子坐标系中的位置
- `assign_child_resource()`: 将仓库资源分配到桌子的指定位置
- `setup()`: 可选的自动设置方法,初始化时可调用
## 2. 堆栈Warehouse构建
堆栈定义了存储区域的规格和布局,用于放置载具。
### 代码示例 (YB_warehouses.py)
```python
from unilabos.resources.warehouse import WareHouse, YB_warehouse_factory
def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
"""创建BioYond 1x4x4仓库
Args:
name: 仓库名称
Returns:
WareHouse: 仓库对象
"""
return YB_warehouse_factory(
name=name,
num_items_x=1, # X方向位置数量
num_items_y=4, # Y方向位置数量
num_items_z=4, # Z方向位置数量层数
dx=10.0, # X方向起始偏移
dy=10.0, # Y方向起始偏移
dz=10.0, # Z方向起始偏移
item_dx=137.0, # X方向间距
item_dy=96.0, # Y方向间距
item_dz=120.0, # Z方向间距层高
category="warehouse",
)
def bioyond_warehouse_2x2x1(name: str) -> WareHouse:
"""创建BioYond 2x2x1仓库自动堆栈"""
return YB_warehouse_factory(
name=name,
num_items_x=2,
num_items_y=2,
num_items_z=1, # 单层
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="YB_warehouse",
)
```
### 关键要点注释
- `num_items_x/y/z`: 定义仓库在各个方向的位置数量
- `dx/dy/dz`: 第一个位置的起始偏移坐标
- `item_dx/dy/dz`: 相邻位置之间的间距
- `category`: 仓库类别,用于分类管理
- `YB_warehouse_factory`: 统一的仓库创建工厂函数
## 3. 载具Carriers构建
载具是承载瓶子的容器架,定义了瓶子的排列方式和位置。
### 代码示例 (YB_bottle_carriers.py)
```python
from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder, create_ordered_items_2d
from unilabos.resources.itemized_carrier import Bottle, BottleCarrier
from unilabos.resources.bioyond.YB_bottles import YB_pei_ye_xiao_Bottle
def YB_peiyepingxiaoban(name: str) -> BottleCarrier:
"""配液瓶(小)板 - 4x2布局8个位置
Args:
name: 载具名称
Returns:
BottleCarrier: 载具对象包含8个配液瓶位置
"""
# 载具物理尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 65.0
# 瓶位参数
bottle_diameter = 35.0 # 瓶子直径
bottle_spacing_x = 42.0 # X方向瓶子间距
bottle_spacing_y = 35.0 # Y方向瓶子间距
# 计算起始位置 (居中排列)
start_x = (carrier_size_x - (4 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
# 创建瓶位布局4列x2行
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=4, # 4列
num_items_y=2, # 2行
dx=start_x,
dy=start_y,
dz=5.0, # 瓶子底部高度
item_dx=bottle_spacing_x,
item_dy=bottle_spacing_y,
size_x=bottle_diameter,
size_y=bottle_diameter,
size_z=carrier_size_z,
)
# 为每个瓶位设置名称
for k, v in sites.items():
v.name = f"{name}_{v.name}"
# 创建载具对象
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=sites,
model="YB_peiyepingxiaoban",
)
# 设置载具布局参数
carrier.num_items_x = 4
carrier.num_items_y = 2
carrier.num_items_z = 1
# 定义瓶子排列顺序
ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"]
# 为每个位置创建瓶子实例
for i in range(8):
carrier[i] = YB_pei_ye_xiao_Bottle(f"{name}_bottle_{ordering[i]}")
return carrier
```
### 关键要点注释
- `carrier_size_x/y/z`: 载具的物理尺寸
- `bottle_diameter`: 瓶子的直径,用于计算瓶位大小
- `bottle_spacing_x/y`: 瓶子之间的间距
- `create_ordered_items_2d`: 创建二维排列的瓶位
- `sites`: 瓶位字典,存储所有瓶子位置信息
- `ordering`: 定义瓶位的命名规则如A1, A2, B1等
## 4. 瓶子Bottles构建
瓶子是最终的物料容器,定义了容器的物理属性。
### 代码示例 (YB_bottles.py)
```python
from unilabos.resources.itemized_carrier import Bottle
def YB_pei_ye_xiao_Bottle(
name: str,
diameter: float = 35.0, # 瓶子直径 (mm)
height: float = 60.0, # 瓶子高度 (mm)
max_volume: float = 30000.0, # 最大容量 (μL) - 30mL
barcode: str = None, # 条码
) -> Bottle:
"""创建配液瓶(小)
Args:
name: 瓶子名称
diameter: 瓶子直径
height: 瓶子高度
max_volume: 最大容量(微升)
barcode: 条码标识
Returns:
Bottle: 瓶子对象
"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_pei_ye_xiao_Bottle",
)
def YB_ye_Bottle(
name: str,
diameter: float = 40.0,
height: float = 70.0,
max_volume: float = 50000.0, # 最大容量
barcode: str = None,
) -> Bottle:
"""创建液体瓶"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_ye_Bottle",
)
```
### 关键要点注释
- `diameter`: 瓶子直径,影响瓶位大小计算
- `height`: 瓶子高度,用于碰撞检测和移液计算
- `max_volume`: 最大容量单位为微升μL
- `barcode`: 条码标识,用于瓶子追踪
- `model`: 型号标识,用于区分不同类型的瓶子
## 5. 注册表配置
创建完物料定义后,需要在注册表中注册这些物料,使系统能够识别和使用它们。
`unilabos/registry/resources/bioyond/` 目录下创建:
- `deck.yaml` - 桌子注册表
- `YB_bottle_carriers.yaml` - 载具注册表
- `YB_bottle.yaml` - 瓶子注册表
### 5.1 桌子注册表 (deck.yaml)
```yaml
BIOYOND_YB_Deck:
category:
- deck # 前端显示的分类存放
class:
module: unilabos.resources.bioyond.decks:BIOYOND_YB_Deck # 定义桌子的类的路径
type: pylabrobot
description: BIOYOND_YB_Deck # 描述信息
handles: []
icon: 配液站.webp # 图标文件
init_param_schema: {}
registry_type: resource # 注册类型
version: 1.0.0 # 版本号
```
### 5.2 载具注册表 (YB_bottle_carriers.yaml)
```yaml
YB_peiyepingxiaoban:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_peiyepingxiaoban
type: pylabrobot
description: YB_peiyepingxiaoban
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
```
### 5.3 瓶子注册表 (YB_bottle.yaml)
```yaml
YB_pei_ye_xiao_Bottle:
category:
- yb3
- YB_bottle
class:
module: unilabos.resources.bioyond.YB_bottles:YB_pei_ye_xiao_Bottle
type: pylabrobot
description: YB_pei_ye_xiao_Bottle
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
```
### 注册表关键要点注释
- `category`: 物料分类,用于在云端(网页界面)中的分类中显示
- `module`: Python模块路径格式为 `模块路径:类名`
- `type`: 框架类型,通常为 `pylabrobot`(默认即可)
- `description`: 描述信息,显示在用户界面中
- `icon`: (名称唯一自动匹配后端上传的图标文件名,显示在云端)
- `registry_type`: 固定为 `resource`
- `version`: 版本号,用于版本管理

File diff suppressed because it is too large Load Diff

View File

@@ -26,15 +26,19 @@ boot_examples/index.md
## 开发者指南
```{toctree}
:maxdepth: 2
developer_guide/device_driver
developer_guide/add_device
developer_guide/add_action
developer_guide/actions
developer_guide/workstation_architecture
developer_guide/add_protocol
developer_guide/add_batteryPLC
developer_guide/materials_tutorial.md
developer_guide/materials_tutorial
developer_guide/materials_construction_guide
```
## 接口文档

View File

@@ -2,6 +2,7 @@
sphinx>=7.0.0
sphinx-rtd-theme>=2.0.0
myst-parser>=2.0.0
sphinxcontrib-mermaid
# 用于支持Jupyter notebook文档
myst-nb>=1.0.0

View File

@@ -1,6 +1,6 @@
package:
name: ros-humble-unilabos-msgs
version: 0.10.7
version: 0.10.10
source:
path: ../../unilabos_msgs
target_directory: src

View File

@@ -0,0 +1,41 @@
:: Generated by vinca http://github.com/RoboStack/vinca.
:: DO NOT EDIT!
setlocal EnableDelayedExpansion
set "PYTHONPATH=%LIBRARY_PREFIX%\lib\site-packages;%SP_DIR%"
:: MSVC is preferred.
set CC=cl.exe
set CXX=cl.exe
rd /s /q build
mkdir build
pushd build
:: set "CMAKE_GENERATOR=Ninja"
:: try to fix long paths issues by using default generator
set "CMAKE_GENERATOR=Visual Studio %VS_MAJOR% %VS_YEAR%"
set "SP_DIR_FORWARDSLASHES=%SP_DIR:\=/%"
set PYTHON="%PREFIX%\python.exe"
cmake ^
-G "%CMAKE_GENERATOR%" ^
-DCMAKE_INSTALL_PREFIX=%LIBRARY_PREFIX% ^
-DCMAKE_BUILD_TYPE=Release ^
-DCMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP=True ^
-DPYTHON_EXECUTABLE=%PYTHON% ^
-DPython_EXECUTABLE=%PYTHON% ^
-DPython3_EXECUTABLE=%PYTHON% ^
-DSETUPTOOLS_DEB_LAYOUT=OFF ^
-DBUILD_SHARED_LIBS=ON ^
-DBUILD_TESTING=OFF ^
-DCMAKE_OBJECT_PATH_MAX=255 ^
-DPYTHON_INSTALL_DIR=%SP_DIR_FORWARDSLASHES% ^
--compile-no-warning-as-error ^
%SRC_DIR%\%PKG_NAME%\src\work
if errorlevel 1 exit 1
cmake --build . --config Release --target install
if errorlevel 1 exit 1

View File

@@ -0,0 +1,71 @@
# Generated by vinca http://github.com/RoboStack/vinca.
# DO NOT EDIT!
rm -rf build
mkdir build
cd build
# necessary for correctly linking SIP files (from python_qt_bindings)
export LINK=$CXX
if [[ "$CONDA_BUILD_CROSS_COMPILATION" != "1" ]]; then
PYTHON_EXECUTABLE=$PREFIX/bin/python
PKG_CONFIG_EXECUTABLE=$PREFIX/bin/pkg-config
OSX_DEPLOYMENT_TARGET="10.15"
else
PYTHON_EXECUTABLE=$BUILD_PREFIX/bin/python
PKG_CONFIG_EXECUTABLE=$BUILD_PREFIX/bin/pkg-config
OSX_DEPLOYMENT_TARGET="11.0"
fi
echo "USING PYTHON_EXECUTABLE=${PYTHON_EXECUTABLE}"
echo "USING PKG_CONFIG_EXECUTABLE=${PKG_CONFIG_EXECUTABLE}"
export ROS_PYTHON_VERSION=`$PYTHON_EXECUTABLE -c "import sys; print('%i.%i' % (sys.version_info[0:2]))"`
echo "Using Python ${ROS_PYTHON_VERSION}"
# Fix up SP_DIR which for some reason might contain a path to a wrong Python version
FIXED_SP_DIR=$(echo $SP_DIR | sed -E "s/python[0-9]+\.[0-9]+/python$ROS_PYTHON_VERSION/")
echo "Using site-package dir ${FIXED_SP_DIR}"
# see https://github.com/conda-forge/cross-python-feedstock/issues/24
if [[ "$CONDA_BUILD_CROSS_COMPILATION" == "1" ]]; then
find $PREFIX/lib/cmake -type f -exec sed -i "s~\${_IMPORT_PREFIX}/lib/python${ROS_PYTHON_VERSION}/site-packages~${BUILD_PREFIX}/lib/python${ROS_PYTHON_VERSION}/site-packages~g" {} + || true
find $PREFIX/share/rosidl* -type f -exec sed -i "s~$PREFIX/lib/python${ROS_PYTHON_VERSION}/site-packages~${BUILD_PREFIX}/lib/python${ROS_PYTHON_VERSION}/site-packages~g" {} + || true
find $PREFIX/share/rosidl* -type f -exec sed -i "s~\${_IMPORT_PREFIX}/lib/python${ROS_PYTHON_VERSION}/site-packages~${BUILD_PREFIX}/lib/python${ROS_PYTHON_VERSION}/site-packages~g" {} + || true
find $PREFIX/lib/cmake -type f -exec sed -i "s~message(FATAL_ERROR \"The imported target~message(WARNING \"The imported target~g" {} + || true
fi
if [[ $target_platform =~ linux.* ]]; then
export CFLAGS="${CFLAGS} -D__STDC_FORMAT_MACROS=1"
export CXXFLAGS="${CXXFLAGS} -D__STDC_FORMAT_MACROS=1"
fi;
# Needed for qt-gui-cpp ..
if [[ $target_platform =~ linux.* ]]; then
ln -s $GCC ${BUILD_PREFIX}/bin/gcc
ln -s $GXX ${BUILD_PREFIX}/bin/g++
fi;
cmake \
-G "Ninja" \
-DCMAKE_INSTALL_PREFIX=$PREFIX \
-DCMAKE_PREFIX_PATH=$PREFIX \
-DAMENT_PREFIX_PATH=$PREFIX \
-DCMAKE_INSTALL_LIBDIR=lib \
-DCMAKE_BUILD_TYPE=Release \
-DPYTHON_EXECUTABLE=$PYTHON_EXECUTABLE \
-DPython_EXECUTABLE=$PYTHON_EXECUTABLE \
-DPython3_EXECUTABLE=$PYTHON_EXECUTABLE \
-DPython3_FIND_STRATEGY=LOCATION \
-DPKG_CONFIG_EXECUTABLE=$PKG_CONFIG_EXECUTABLE \
-DPYTHON_INSTALL_DIR=$FIXED_SP_DIR \
-DSETUPTOOLS_DEB_LAYOUT=OFF \
-DCATKIN_SKIP_TESTING=$SKIP_TESTING \
-DCMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP=True \
-DBUILD_SHARED_LIBS=ON \
-DBUILD_TESTING=OFF \
-DCMAKE_OSX_DEPLOYMENT_TARGET=$OSX_DEPLOYMENT_TARGET \
--compile-no-warning-as-error \
$SRC_DIR/$PKG_NAME/src/work
cmake --build . --config Release --target install

View File

@@ -0,0 +1,61 @@
package:
name: ros-humble-unilabos-msgs
version: 0.9.7
source:
path: ../../unilabos_msgs
folder: ros-humble-unilabos-msgs/src/work
build:
script:
sel(win): bld_ament_cmake.bat
sel(unix): build_ament_cmake.sh
number: 5
about:
home: https://www.ros.org/
license: BSD-3-Clause
summary: |
Robot Operating System
extra:
recipe-maintainers:
- ros-forge
requirements:
build:
- "{{ compiler('cxx') }}"
- "{{ compiler('c') }}"
- sel(linux64): sysroot_linux-64 2.17
- ninja
- setuptools
- sel(unix): make
- sel(unix): coreutils
- sel(osx): tapi
- sel(build_platform != target_platform): pkg-config
- cmake
- cython
- sel(win): vs2022_win-64
- sel(build_platform != target_platform): python
- sel(build_platform != target_platform): cross-python_{{ target_platform }}
- sel(build_platform != target_platform): numpy
host:
- numpy
- pip
- sel(build_platform == target_platform): pkg-config
- robostack-staging::ros-humble-action-msgs
- robostack-staging::ros-humble-ament-cmake
- robostack-staging::ros-humble-ament-lint-auto
- robostack-staging::ros-humble-ament-lint-common
- robostack-staging::ros-humble-ros-environment
- robostack-staging::ros-humble-ros-workspace
- robostack-staging::ros-humble-rosidl-default-generators
- robostack-staging::ros-humble-std-msgs
- robostack-staging::ros-humble-geometry-msgs
- robostack-staging::ros2-distro-mutex=0.5.*
run:
- robostack-staging::ros-humble-action-msgs
- robostack-staging::ros-humble-ros-workspace
- robostack-staging::ros-humble-rosidl-default-runtime
- robostack-staging::ros-humble-std-msgs
- robostack-staging::ros-humble-geometry-msgs
# - robostack-staging::ros2-distro-mutex=0.6.*
- sel(osx and x86_64): __osx >={{ MACOSX_DEPLOYMENT_TARGET|default('10.14') }}

View File

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

695
scripts/workflow.py Normal file
View File

@@ -0,0 +1,695 @@
import json
import logging
import traceback
import uuid
import xml.etree.ElementTree as ET
from typing import Any, Dict, List
import networkx as nx
import matplotlib.pyplot as plt
import requests
logger = logging.getLogger(__name__)
class SimpleGraph:
"""简单的有向图实现,用于构建工作流图"""
def __init__(self):
self.nodes = {}
self.edges = []
def add_node(self, node_id, **attrs):
"""添加节点"""
self.nodes[node_id] = attrs
def add_edge(self, source, target, **attrs):
"""添加边"""
edge = {"source": source, "target": target, **attrs}
self.edges.append(edge)
def to_dict(self):
"""转换为工作流图格式"""
nodes_list = []
for node_id, attrs in self.nodes.items():
node_attrs = attrs.copy()
params = node_attrs.pop("parameters", {}) or {}
node_attrs.update(params)
nodes_list.append({"id": node_id, **node_attrs})
return {
"directed": True,
"multigraph": False,
"graph": {},
"nodes": nodes_list,
"links": self.edges,
}
def extract_json_from_markdown(text: str) -> str:
"""从markdown代码块中提取JSON"""
text = text.strip()
if text.startswith("```json\n"):
text = text[8:]
if text.startswith("```\n"):
text = text[4:]
if text.endswith("\n```"):
text = text[:-4]
return text
def convert_to_type(val: str) -> Any:
"""将字符串值转换为适当的数据类型"""
if val == "True":
return True
if val == "False":
return False
if val == "?":
return None
if val.endswith(" g"):
return float(val.split(" ")[0])
if val.endswith("mg"):
return float(val.split("mg")[0])
elif val.endswith("mmol"):
return float(val.split("mmol")[0]) / 1000
elif val.endswith("mol"):
return float(val.split("mol")[0])
elif val.endswith("ml"):
return float(val.split("ml")[0])
elif val.endswith("RPM"):
return float(val.split("RPM")[0])
elif val.endswith(" °C"):
return float(val.split(" ")[0])
elif val.endswith(" %"):
return float(val.split(" ")[0])
return val
def refactor_data(data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""统一的数据重构函数,根据操作类型自动选择模板"""
refactored_data = []
# 定义操作映射,包含生物实验和有机化学的所有操作
OPERATION_MAPPING = {
# 生物实验操作
"transfer_liquid": "SynBioFactory-liquid_handler.prcxi-transfer_liquid",
"transfer": "SynBioFactory-liquid_handler.biomek-transfer",
"incubation": "SynBioFactory-liquid_handler.biomek-incubation",
"move_labware": "SynBioFactory-liquid_handler.biomek-move_labware",
"oscillation": "SynBioFactory-liquid_handler.biomek-oscillation",
# 有机化学操作
"HeatChillToTemp": "SynBioFactory-workstation-HeatChillProtocol",
"StopHeatChill": "SynBioFactory-workstation-HeatChillStopProtocol",
"StartHeatChill": "SynBioFactory-workstation-HeatChillStartProtocol",
"HeatChill": "SynBioFactory-workstation-HeatChillProtocol",
"Dissolve": "SynBioFactory-workstation-DissolveProtocol",
"Transfer": "SynBioFactory-workstation-TransferProtocol",
"Evaporate": "SynBioFactory-workstation-EvaporateProtocol",
"Recrystallize": "SynBioFactory-workstation-RecrystallizeProtocol",
"Filter": "SynBioFactory-workstation-FilterProtocol",
"Dry": "SynBioFactory-workstation-DryProtocol",
"Add": "SynBioFactory-workstation-AddProtocol",
}
UNSUPPORTED_OPERATIONS = ["Purge", "Wait", "Stir", "ResetHandling"]
for step in data:
operation = step.get("action")
if not operation or operation in UNSUPPORTED_OPERATIONS:
continue
# 处理重复操作
if operation == "Repeat":
times = step.get("times", step.get("parameters", {}).get("times", 1))
sub_steps = step.get("steps", step.get("parameters", {}).get("steps", []))
for i in range(int(times)):
sub_data = refactor_data(sub_steps)
refactored_data.extend(sub_data)
continue
# 获取模板名称
template = OPERATION_MAPPING.get(operation)
if not template:
# 自动推断模板类型
if operation.lower() in ["transfer", "incubation", "move_labware", "oscillation"]:
template = f"SynBioFactory-liquid_handler.biomek-{operation}"
else:
template = f"SynBioFactory-workstation-{operation}Protocol"
# 创建步骤数据
step_data = {
"template": template,
"description": step.get("description", step.get("purpose", f"{operation} operation")),
"lab_node_type": "Device",
"parameters": step.get("parameters", step.get("action_args", {})),
}
refactored_data.append(step_data)
return refactored_data
def build_protocol_graph(
labware_info: List[Dict[str, Any]], protocol_steps: List[Dict[str, Any]], workstation_name: str
) -> SimpleGraph:
"""统一的协议图构建函数,根据设备类型自动选择构建逻辑"""
G = SimpleGraph()
resource_last_writer = {}
LAB_NAME = "SynBioFactory"
protocol_steps = refactor_data(protocol_steps)
# 检查协议步骤中的模板来判断协议类型
has_biomek_template = any(
("biomek" in step.get("template", "")) or ("prcxi" in step.get("template", ""))
for step in protocol_steps
)
if has_biomek_template:
# 生物实验协议图构建
for labware_id, labware in labware_info.items():
node_id = str(uuid.uuid4())
labware_attrs = labware.copy()
labware_id = labware_attrs.pop("id", labware_attrs.get("name", f"labware_{uuid.uuid4()}"))
labware_attrs["description"] = labware_id
labware_attrs["lab_node_type"] = (
"Reagent" if "Plate" in str(labware_id) else "Labware" if "Rack" in str(labware_id) else "Sample"
)
labware_attrs["device_id"] = workstation_name
G.add_node(node_id, template=f"{LAB_NAME}-host_node-create_resource", **labware_attrs)
resource_last_writer[labware_id] = f"{node_id}:labware"
# 处理协议步骤
prev_node = None
for i, step in enumerate(protocol_steps):
node_id = str(uuid.uuid4())
G.add_node(node_id, **step)
# 添加控制流边
if prev_node is not None:
G.add_edge(prev_node, node_id, source_port="ready", target_port="ready")
prev_node = node_id
# 处理物料流
params = step.get("parameters", {})
if "sources" in params and params["sources"] in resource_last_writer:
source_node, source_port = resource_last_writer[params["sources"]].split(":")
G.add_edge(source_node, node_id, source_port=source_port, target_port="labware")
if "targets" in params:
resource_last_writer[params["targets"]] = f"{node_id}:labware"
# 添加协议结束节点
end_id = str(uuid.uuid4())
G.add_node(end_id, template=f"{LAB_NAME}-liquid_handler.biomek-run_protocol")
if prev_node is not None:
G.add_edge(prev_node, end_id, source_port="ready", target_port="ready")
else:
# 有机化学协议图构建
WORKSTATION_ID = workstation_name
# 为所有labware创建资源节点
for item_id, item in labware_info.items():
# item_id = item.get("id") or item.get("name", f"item_{uuid.uuid4()}")
node_id = str(uuid.uuid4())
# 判断节点类型
if item.get("type") == "hardware" or "reactor" in str(item_id).lower():
if "reactor" not in str(item_id).lower():
continue
lab_node_type = "Sample"
description = f"Prepare Reactor: {item_id}"
liquid_type = []
liquid_volume = []
else:
lab_node_type = "Reagent"
description = f"Add Reagent to Flask: {item_id}"
liquid_type = [item_id]
liquid_volume = [1e5]
G.add_node(
node_id,
template=f"{LAB_NAME}-host_node-create_resource",
description=description,
lab_node_type=lab_node_type,
res_id=item_id,
device_id=WORKSTATION_ID,
class_name="container",
parent=WORKSTATION_ID,
bind_locations={"x": 0.0, "y": 0.0, "z": 0.0},
liquid_input_slot=[-1],
liquid_type=liquid_type,
liquid_volume=liquid_volume,
slot_on_deck="",
role=item.get("role", ""),
)
resource_last_writer[item_id] = f"{node_id}:labware"
last_control_node_id = None
# 处理协议步骤
for step in protocol_steps:
node_id = str(uuid.uuid4())
G.add_node(node_id, **step)
# 控制流
if last_control_node_id is not None:
G.add_edge(last_control_node_id, node_id, source_port="ready", target_port="ready")
last_control_node_id = node_id
# 物料流
params = step.get("parameters", {})
input_resources = {
"Vessel": params.get("vessel"),
"ToVessel": params.get("to_vessel"),
"FromVessel": params.get("from_vessel"),
"reagent": params.get("reagent"),
"solvent": params.get("solvent"),
"compound": params.get("compound"),
"sources": params.get("sources"),
"targets": params.get("targets"),
}
for target_port, resource_name in input_resources.items():
if resource_name and resource_name in resource_last_writer:
source_node, source_port = resource_last_writer[resource_name].split(":")
G.add_edge(source_node, node_id, source_port=source_port, target_port=target_port)
output_resources = {
"VesselOut": params.get("vessel"),
"FromVesselOut": params.get("from_vessel"),
"ToVesselOut": params.get("to_vessel"),
"FiltrateOut": params.get("filtrate_vessel"),
"reagent": params.get("reagent"),
"solvent": params.get("solvent"),
"compound": params.get("compound"),
"sources_out": params.get("sources"),
"targets_out": params.get("targets"),
}
for source_port, resource_name in output_resources.items():
if resource_name:
resource_last_writer[resource_name] = f"{node_id}:{source_port}"
return G
def draw_protocol_graph(protocol_graph: SimpleGraph, output_path: str):
"""
(辅助功能) 使用 networkx 和 matplotlib 绘制协议工作流图,用于可视化。
"""
if not protocol_graph:
print("Cannot draw graph: Graph object is empty.")
return
G = nx.DiGraph()
for node_id, attrs in protocol_graph.nodes.items():
label = attrs.get("description", attrs.get("template", node_id[:8]))
G.add_node(node_id, label=label, **attrs)
for edge in protocol_graph.edges:
G.add_edge(edge["source"], edge["target"])
plt.figure(figsize=(20, 15))
try:
pos = nx.nx_agraph.graphviz_layout(G, prog="dot")
except Exception:
pos = nx.shell_layout(G) # Fallback layout
node_labels = {node: data["label"] for node, data in G.nodes(data=True)}
nx.draw(
G,
pos,
with_labels=False,
node_size=2500,
node_color="skyblue",
node_shape="o",
edge_color="gray",
width=1.5,
arrowsize=15,
)
nx.draw_networkx_labels(G, pos, labels=node_labels, font_size=8, font_weight="bold")
plt.title("Chemical Protocol Workflow Graph", size=15)
plt.savefig(output_path, dpi=300, bbox_inches="tight")
plt.close()
print(f" - Visualization saved to '{output_path}'")
from networkx.drawing.nx_agraph import to_agraph
import re
COMPASS = {"n","e","s","w","ne","nw","se","sw","c"}
def _is_compass(port: str) -> bool:
return isinstance(port, str) and port.lower() in COMPASS
def draw_protocol_graph_with_ports(protocol_graph, output_path: str, rankdir: str = "LR"):
"""
使用 Graphviz 端口语法绘制协议工作流图。
- 若边上的 source_port/target_port 是 compassn/e/s/w/...),直接用 compass。
- 否则自动为节点创建 record 形状并定义命名端口 <portname>。
最终由 PyGraphviz 渲染并输出到 output_path后缀决定格式如 .png/.svg/.pdf
"""
if not protocol_graph:
print("Cannot draw graph: Graph object is empty.")
return
# 1) 先用 networkx 搭建有向图,保留端口属性
G = nx.DiGraph()
for node_id, attrs in protocol_graph.nodes.items():
label = attrs.get("description", attrs.get("template", node_id[:8]))
# 保留一个干净的“中心标签”,用于放在 record 的中间槽
G.add_node(node_id, _core_label=str(label), **{k:v for k,v in attrs.items() if k not in ("label",)})
edges_data = []
in_ports_by_node = {} # 收集命名输入端口
out_ports_by_node = {} # 收集命名输出端口
for edge in protocol_graph.edges:
u = edge["source"]
v = edge["target"]
sp = edge.get("source_port")
tp = edge.get("target_port")
# 记录到图里(保留原始端口信息)
G.add_edge(u, v, source_port=sp, target_port=tp)
edges_data.append((u, v, sp, tp))
# 如果不是 compass就按“命名端口”先归类等会儿给节点造 record
if sp and not _is_compass(sp):
out_ports_by_node.setdefault(u, set()).add(str(sp))
if tp and not _is_compass(tp):
in_ports_by_node.setdefault(v, set()).add(str(tp))
# 2) 转为 AGraph使用 Graphviz 渲染
A = to_agraph(G)
A.graph_attr.update(rankdir=rankdir, splines="true", concentrate="false", fontsize="10")
A.node_attr.update(shape="box", style="rounded,filled", fillcolor="lightyellow", color="#999999", fontname="Helvetica")
A.edge_attr.update(arrowsize="0.8", color="#666666")
# 3) 为需要命名端口的节点设置 record 形状与 label
# 左列 = 输入端口;中间 = 核心标签;右列 = 输出端口
for n in A.nodes():
node = A.get_node(n)
core = G.nodes[n].get("_core_label", n)
in_ports = sorted(in_ports_by_node.get(n, []))
out_ports = sorted(out_ports_by_node.get(n, []))
# 如果该节点涉及命名端口,则用 record否则保留原 box
if in_ports or out_ports:
def port_fields(ports):
if not ports:
return " " # 必须留一个空槽占位
# 每个端口一个小格子,<p> name
return "|".join(f"<{re.sub(r'[^A-Za-z0-9_:.|-]', '_', p)}> {p}" for p in ports)
left = port_fields(in_ports)
right = port_fields(out_ports)
# 三栏:左(入) | 中(节点名) | 右(出)
record_label = f"{{ {left} | {core} | {right} }}"
node.attr.update(shape="record", label=record_label)
else:
# 没有命名端口:普通盒子,显示核心标签
node.attr.update(label=str(core))
# 4) 给边设置 headport / tailport
# - 若端口为 compass直接用 compasse.g., headport="e"
# - 若端口为命名端口:使用在 record 中定义的 <port> 名(同名即可)
for (u, v, sp, tp) in edges_data:
e = A.get_edge(u, v)
# Graphviz 属性tail 是源head 是目标
if sp:
if _is_compass(sp):
e.attr["tailport"] = sp.lower()
else:
# 与 record label 中 <port> 名一致;特殊字符已在 label 中做了清洗
e.attr["tailport"] = re.sub(r'[^A-Za-z0-9_:.|-]', '_', str(sp))
if tp:
if _is_compass(tp):
e.attr["headport"] = tp.lower()
else:
e.attr["headport"] = re.sub(r'[^A-Za-z0-9_:.|-]', '_', str(tp))
# 可选:若想让边更贴边缘,可设置 constraint/spline 等
# e.attr["arrowhead"] = "vee"
# 5) 输出
A.draw(output_path, prog="dot")
print(f" - Port-aware workflow rendered to '{output_path}'")
def flatten_xdl_procedure(procedure_elem: ET.Element) -> List[ET.Element]:
"""展平嵌套的XDL程序结构"""
flattened_operations = []
TEMP_UNSUPPORTED_PROTOCOL = ["Purge", "Wait", "Stir", "ResetHandling"]
def extract_operations(element: ET.Element):
if element.tag not in ["Prep", "Reaction", "Workup", "Purification", "Procedure"]:
if element.tag not in TEMP_UNSUPPORTED_PROTOCOL:
flattened_operations.append(element)
for child in element:
extract_operations(child)
for child in procedure_elem:
extract_operations(child)
return flattened_operations
def parse_xdl_content(xdl_content: str) -> tuple:
"""解析XDL内容"""
try:
xdl_content_cleaned = "".join(c for c in xdl_content if c.isprintable())
root = ET.fromstring(xdl_content_cleaned)
synthesis_elem = root.find("Synthesis")
if synthesis_elem is None:
return None, None, None
# 解析硬件组件
hardware_elem = synthesis_elem.find("Hardware")
hardware = []
if hardware_elem is not None:
hardware = [{"id": c.get("id"), "type": c.get("type")} for c in hardware_elem.findall("Component")]
# 解析试剂
reagents_elem = synthesis_elem.find("Reagents")
reagents = []
if reagents_elem is not None:
reagents = [{"name": r.get("name"), "role": r.get("role", "")} for r in reagents_elem.findall("Reagent")]
# 解析程序
procedure_elem = synthesis_elem.find("Procedure")
if procedure_elem is None:
return None, None, None
flattened_operations = flatten_xdl_procedure(procedure_elem)
return hardware, reagents, flattened_operations
except ET.ParseError as e:
raise ValueError(f"Invalid XDL format: {e}")
def convert_xdl_to_dict(xdl_content: str) -> Dict[str, Any]:
"""
将XDL XML格式转换为标准的字典格式
Args:
xdl_content: XDL XML内容
Returns:
转换结果,包含步骤和器材信息
"""
try:
hardware, reagents, flattened_operations = parse_xdl_content(xdl_content)
if hardware is None:
return {"error": "Failed to parse XDL content", "success": False}
# 将XDL元素转换为字典格式
steps_data = []
for elem in flattened_operations:
# 转换参数类型
parameters = {}
for key, val in elem.attrib.items():
converted_val = convert_to_type(val)
if converted_val is not None:
parameters[key] = converted_val
step_dict = {
"operation": elem.tag,
"parameters": parameters,
"description": elem.get("purpose", f"Operation: {elem.tag}"),
}
steps_data.append(step_dict)
# 合并硬件和试剂为统一的labware_info格式
labware_data = []
labware_data.extend({"id": hw["id"], "type": "hardware", **hw} for hw in hardware)
labware_data.extend({"name": reagent["name"], "type": "reagent", **reagent} for reagent in reagents)
return {
"success": True,
"steps": steps_data,
"labware": labware_data,
"message": f"Successfully converted XDL to dict format. Found {len(steps_data)} steps and {len(labware_data)} labware items.",
}
except Exception as e:
error_msg = f"XDL conversion failed: {str(e)}"
logger.error(error_msg)
return {"error": error_msg, "success": False}
def create_workflow(
steps_info: str,
labware_info: str,
workflow_name: str = "Generated Workflow",
workstation_name: str = "workstation",
workflow_description: str = "Auto-generated workflow from protocol",
) -> Dict[str, Any]:
"""
创建工作流,输入数据已经是统一的字典格式
Args:
steps_info: 步骤信息 (JSON字符串已经是list of dict格式)
labware_info: 实验器材和试剂信息 (JSON字符串已经是list of dict格式)
workflow_name: 工作流名称
workflow_description: 工作流描述
Returns:
创建结果包含工作流UUID和详细信息
"""
try:
# 直接解析JSON数据
steps_info_clean = extract_json_from_markdown(steps_info)
labware_info_clean = extract_json_from_markdown(labware_info)
steps_data = json.loads(steps_info_clean)
labware_data = json.loads(labware_info_clean)
# 统一处理所有数据
protocol_graph = build_protocol_graph(labware_data, steps_data, workstation_name=workstation_name)
# 检测协议类型(用于标签)
protocol_type = "bio" if any("biomek" in step.get("template", "") for step in refactored_steps) else "organic"
# 转换为工作流格式
data = protocol_graph.to_dict()
# 转换节点格式
for i, node in enumerate(data["nodes"]):
description = node.get("description", "")
onode = {
"template": node.pop("template"),
"id": node["id"],
"lab_node_type": node.get("lab_node_type", "Device"),
"name": description or f"Node {i + 1}",
"params": {"default": node},
"handles": {},
}
# 处理边连接
for edge in data["links"]:
if edge["source"] == node["id"]:
source_port = edge.get("source_port", "output")
if source_port not in onode["handles"]:
onode["handles"][source_port] = {"type": "source"}
if edge["target"] == node["id"]:
target_port = edge.get("target_port", "input")
if target_port not in onode["handles"]:
onode["handles"][target_port] = {"type": "target"}
data["nodes"][i] = onode
# 发送到API创建工作流
api_secret = configs.Lab.Key
if not api_secret:
return {"error": "API SecretKey is not configured", "success": False}
# Step 1: 创建工作流
workflow_url = f"{configs.Lab.Api}/api/v1/workflow/"
headers = {
"Content-Type": "application/json",
}
params = {"secret_key": api_secret}
graph_data = {"name": workflow_name, **data}
logger.info(f"Creating workflow: {workflow_name}")
response = requests.post(
workflow_url, params=params, json=graph_data, headers=headers, timeout=configs.Lab.Timeout
)
response.raise_for_status()
workflow_info = response.json()
if workflow_info.get("code") != 0:
error_msg = f"API returned an error: {workflow_info.get('msg', 'Unknown Error')}"
logger.error(error_msg)
return {"error": error_msg, "success": False}
workflow_uuid = workflow_info.get("data", {}).get("uuid")
if not workflow_uuid:
return {"error": "Failed to get workflow UUID from response", "success": False}
# Step 2: 添加到模板库(可选)
try:
library_url = f"{configs.Lab.Api}/api/flociety/vs/workflows/library/"
lib_payload = {
"workflow_uuid": workflow_uuid,
"title": workflow_name,
"description": workflow_description,
"labels": [protocol_type.title(), "Auto-generated"],
}
library_response = requests.post(
library_url, params=params, json=lib_payload, headers=headers, timeout=configs.Lab.Timeout
)
library_response.raise_for_status()
library_info = library_response.json()
logger.info(f"Workflow added to library: {library_info}")
return {
"success": True,
"workflow_uuid": workflow_uuid,
"workflow_info": workflow_info.get("data"),
"library_info": library_info.get("data"),
"protocol_type": protocol_type,
"message": f"Workflow '{workflow_name}' created successfully",
}
except Exception as e:
# 即使添加到库失败,工作流创建仍然成功
logger.warning(f"Failed to add workflow to library: {str(e)}")
return {
"success": True,
"workflow_uuid": workflow_uuid,
"workflow_info": workflow_info.get("data"),
"protocol_type": protocol_type,
"message": f"Workflow '{workflow_name}' created successfully (library addition failed)",
}
except requests.exceptions.RequestException as e:
error_msg = f"Network error when calling API: {str(e)}"
logger.error(error_msg)
return {"error": error_msg, "success": False}
except json.JSONDecodeError as e:
error_msg = f"JSON parsing error: {str(e)}"
logger.error(error_msg)
return {"error": error_msg, "success": False}
except Exception as e:
error_msg = f"An unexpected error occurred: {str(e)}"
logger.error(error_msg)
logger.error(traceback.format_exc())
return {"error": error_msg, "success": False}

View File

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

View File

@@ -0,0 +1,171 @@
{
"nodes": [
{
"id": "dispensing_station_bioyond",
"name": "dispensing_station_bioyond",
"children": [
"Bioyond_Dispensing_Deck"
],
"parent": null,
"type": "device",
"class": "bioyond_dispensing_station",
"config": {
"config": {
"api_key": "DE9BDDA0",
"api_host": "http://192.168.1.200:44400",
"material_type_mappings": {
"BIOYOND_PolymerStation_1FlaskCarrier": [
"烧杯",
"3a14196b-24f2-ca49-9081-0cab8021bf1a"
],
"BIOYOND_PolymerStation_1BottleCarrier": [
"试剂瓶",
"3a14196b-8bcf-a460-4f74-23f21ca79e72"
],
"BIOYOND_PolymerStation_6StockCarrier": [
"分装板",
"3a14196e-5dfe-6e21-0c79-fe2036d052c4"
],
"BIOYOND_PolymerStation_Liquid_Vial": [
"10%分装小瓶",
"3a14196c-76be-2279-4e22-7310d69aed68"
],
"BIOYOND_PolymerStation_Solid_Vial": [
"90%分装小瓶",
"3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"
],
"BIOYOND_PolymerStation_8StockCarrier": [
"样品板",
"3a14196e-b7a0-a5da-1931-35f3000281e9"
],
"BIOYOND_PolymerStation_Solid_Stock": [
"样品瓶",
"3a14196a-cf7d-8aea-48d8-b9662c7dba94"
]
}
},
"deck": {
"data": {
"_resource_child_name": "Bioyond_Dispensing_Deck",
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerPreparationStation_Deck"
}
},
"protocol_type": []
},
"data": {}
},
{
"id": "Bioyond_Dispensing_Deck",
"name": "Bioyond_Dispensing_Deck",
"sample_id": null,
"children": [],
"parent": "dispensing_station_bioyond",
"type": "deck",
"class": "BIOYOND_PolymerPreparationStation_Deck",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"type": "BIOYOND_PolymerPreparationStation_Deck",
"setup": true,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
}
},
"data": {}
},
{
"id": "reaction_station_bioyond",
"name": "reaction_station_bioyond",
"parent": null,
"children": [
"Bioyond_Deck"
],
"type": "device",
"class": "reaction_station.bioyond",
"config": {
"config": {
"api_key": "DE9BDDA0",
"api_host": "http://192.168.1.200:44402",
"workflow_mappings": {
"reactor_taken_out": "3a16081e-4788-ca37-eff4-ceed8d7019d1",
"reactor_taken_in": "3a160df6-76b3-0957-9eb0-cb496d5721c6",
"Solid_feeding_vials": "3a160877-87e7-7699-7bc6-ec72b05eb5e6",
"Liquid_feeding_vials(non-titration)": "3a167d99-6158-c6f0-15b5-eb030f7d8e47",
"Liquid_feeding_solvents": "3a160824-0665-01ed-285a-51ef817a9046",
"Liquid_feeding(titration)": "3a16082a-96ac-0449-446a-4ed39f3365b6",
"liquid_feeding_beaker": "3a16087e-124f-8ddb-8ec1-c2dff09ca784",
"Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a"
},
"material_type_mappings": {
"BIOYOND_PolymerStation_Reactor": [
"反应器",
"3a14233b-902d-0d7b-4533-3f60f1c41c1b"
],
"BIOYOND_PolymerStation_1BottleCarrier": [
"试剂瓶",
"3a14233b-56e3-6c53-a8ab-fcaac163a9ba"
],
"BIOYOND_PolymerStation_1FlaskCarrier": [
"烧杯",
"3a14233b-f0a9-ba84-eaa9-0d4718b361b6"
],
"BIOYOND_PolymerStation_6StockCarrier": [
"样品板",
"3a142339-80de-8f25-6093-1b1b1b6c322e"
],
"BIOYOND_PolymerStation_Solid_Vial": [
"90%分装小瓶",
"3a14233a-26e1-28f8-af6a-60ca06ba0165"
],
"BIOYOND_PolymerStation_Liquid_Vial": [
"10%分装小瓶",
"3a14233a-84a3-088d-6676-7cb4acd57c64"
],
"BIOYOND_PolymerStation_TipBox": [
"枪头盒",
"3a143890-9d51-60ac-6d6f-6edb43c12041"
]
}
},
"deck": {
"data": {
"_resource_child_name": "Bioyond_Deck",
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerReactionStation_Deck"
}
},
"protocol_type": []
},
"data": {}
},
{
"id": "Bioyond_Deck",
"name": "Bioyond_Deck",
"children": [],
"parent": "reaction_station_bioyond",
"type": "deck",
"class": "BIOYOND_PolymerReactionStation_Deck",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"type": "BIOYOND_PolymerReactionStation_Deck",
"setup": true,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
}
},
"data": {}
}
]
}

View File

@@ -8,11 +8,41 @@
],
"parent": null,
"type": "device",
"class": "workstation.bioyond_dispensing_station",
"class": "bioyond_dispensing_station",
"config": {
"config": {
"api_key": "DE9BDDA0",
"api_host": "http://192.168.1.200:44388"
"api_host": "http://192.168.1.200:44388",
"material_type_mappings": {
"BIOYOND_PolymerStation_1FlaskCarrier": [
"烧杯",
"3a14196b-24f2-ca49-9081-0cab8021bf1a"
],
"BIOYOND_PolymerStation_1BottleCarrier": [
"试剂瓶",
"3a14196b-8bcf-a460-4f74-23f21ca79e72"
],
"BIOYOND_PolymerStation_6StockCarrier": [
"分装板",
"3a14196e-5dfe-6e21-0c79-fe2036d052c4"
],
"BIOYOND_PolymerStation_Liquid_Vial": [
"10%分装小瓶",
"3a14196c-76be-2279-4e22-7310d69aed68"
],
"BIOYOND_PolymerStation_Solid_Vial": [
"90%分装小瓶",
"3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"
],
"BIOYOND_PolymerStation_8StockCarrier": [
"样品板",
"3a14196e-b7a0-a5da-1931-35f3000281e9"
],
"BIOYOND_PolymerStation_Solid_Stock": [
"样品瓶",
"3a14196a-cf7d-8aea-48d8-b9662c7dba94"
]
}
},
"deck": {
"data": {
@@ -50,4 +80,4 @@
"data": {}
}
]
}
}

View File

@@ -0,0 +1,19 @@
{
"nodes": [
{
"id": "id",
"name": "name",
"children": [
],
"parent": null,
"type": "device",
"class": "opcua_example",
"config": {
"url": "url",
"config_path": "unilabos/device_comms/opcua_client/opcua_workflow_example.json"
},
"data": {
}
}
]
}

View File

@@ -10,24 +10,22 @@
"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"
]
],
"config": {
"deck": {
"_resource_child_name": "deck",
"_resource_type": "pylabrobot.resources.opentrons.deck:OTDeck"
},
"backend": {
"type": "UniLiquidHandlerRvizBackend"
},
"simulator": true
}
},
{
"id": "deck",
@@ -9650,7 +9648,7 @@
"children": [],
"parent": null,
"type": "device",
"class": "robotic_arm.SCARA_with_slider.virtual",
"class": "robotic_arm.SCARA_with_slider.moveit.virtual",
"position": {
"x": -500,
"y": 1000,

File diff suppressed because it is too large Load Diff

View File

@@ -24,13 +24,34 @@
"Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a"
},
"material_type_mappings": {
"烧杯": ["BIOYOND_PolymerStation_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"],
"试剂瓶": ["BIOYOND_PolymerStation_1BottleCarrier", ""],
"样品板": ["BIOYOND_PolymerStation_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"],
"分装板": ["BIOYOND_PolymerStation_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"],
"样品瓶": ["BIOYOND_PolymerStation_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"],
"90%分装小瓶": ["BIOYOND_PolymerStation_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"],
"10%分装小瓶": ["BIOYOND_PolymerStation_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"]
"BIOYOND_PolymerStation_Reactor": [
"反应器",
"3a14233b-902d-0d7b-4533-3f60f1c41c1b"
],
"BIOYOND_PolymerStation_1BottleCarrier": [
"试剂瓶",
"3a14233b-56e3-6c53-a8ab-fcaac163a9ba"
],
"BIOYOND_PolymerStation_1FlaskCarrier": [
"烧杯",
"3a14233b-f0a9-ba84-eaa9-0d4718b361b6"
],
"BIOYOND_PolymerStation_6StockCarrier": [
"样品板",
"3a142339-80de-8f25-6093-1b1b1b6c322e"
],
"BIOYOND_PolymerStation_Solid_Vial": [
"90%分装小瓶",
"3a14233a-26e1-28f8-af6a-60ca06ba0165"
],
"BIOYOND_PolymerStation_Liquid_Vial": [
"10%分装小瓶",
"3a14233a-84a3-088d-6676-7cb4acd57c64"
],
"BIOYOND_PolymerStation_TipBox": [
"枪头盒",
"3a143890-9d51-60ac-6d6f-6edb43c12041"
]
}
},
"deck": {
@@ -46,8 +67,7 @@
{
"id": "Bioyond_Deck",
"name": "Bioyond_Deck",
"children": [
],
"children": [],
"parent": "reaction_station_bioyond",
"type": "deck",
"class": "BIOYOND_PolymerReactionStation_Deck",

View File

@@ -18,21 +18,21 @@
"config": {
"deck": {
"_resource_child_name": "deck",
"_resource_type": "pylabrobot.resources.opentrons.deck:OTDeck",
"_resource_type": "unilabos.devices.liquid_handling.laiyu.laiyu:TransformXYZDeck",
"name": "deck"
},
"backend": {
"type": "UniLiquidHandlerRvizBackend"
"type": "UniLiquidHandlerLaiyuBackend",
"port": "/dev/ttyUSB_CH340"
},
"simulator": true,
"total_height": 300
"simulator": false,
"total_height": 232.5
}
},
{
"id": "deck",
"name": "deck",
"sample_id": null,
"children": [
"tip_rack",
"plate_well",
@@ -64,7 +64,7 @@
{
"id": "tip_rack",
"name": "tip_rack",
"sample_id": null,
"children": [
"tip_rack_A1"
],
@@ -102,7 +102,7 @@
{
"id": "tip_rack_A1",
"name": "tip_rack_A1",
"sample_id": null,
"children": [],
"parent": "tip_rack",
"type": "container",
@@ -144,7 +144,7 @@
{
"id": "plate_well",
"name": "plate_well",
"sample_id": null,
"children": [
"plate_well_A1"
],
@@ -156,18 +156,6 @@
"y": 116,
"z": 48.5
},
"pose": {
"position_3d": {
"x": 161,
"y": 116,
"z": 48.5
},
"rotation": {
"x": 0,
"y": 0,
"z": 0
}
},
"config": {
"type": "Plate",
"size_x": 127.76,
@@ -195,7 +183,7 @@
{
"id": "plate_well_A1",
"name": "plate_well_A1",
"sample_id": null,
"children": [],
"parent": "plate_well",
"type": "device",
@@ -236,7 +224,7 @@
{
"id": "tube_rack",
"name": "tube_rack",
"sample_id": null,
"children": [
"tube_rack_A1"
],
@@ -271,7 +259,7 @@
{
"id": "tube_rack_A1",
"name": "tube_rack_A1",
"sample_id": null,
"children": [],
"parent": "tube_rack",
"type": "device",
@@ -315,7 +303,7 @@
{
"id": "bottle_rack",
"name": "bottle_rack",
"sample_id": null,
"children": [
"bottle_rack_A1"
],
@@ -351,7 +339,7 @@
{
"id": "bottle_rack_A1",
"name": "bottle_rack_A1",
"sample_id": null,
"children": [],
"parent": "bottle_rack",
"type": "device",

View File

@@ -0,0 +1,383 @@
{
"nodes": [
{
"id": "liquid_handler",
"name": "liquid_handler",
"parent": null,
"type": "device",
"class": "liquid_handler",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"data": {},
"children": [
"deck"
],
"config": {
"deck": {
"_resource_child_name": "deck",
"_resource_type": "unilabos.devices.liquid_handling.laiyu.laiyu:TransformXYZDeck",
"name": "deck"
},
"backend": {
"type": "UniLiquidHandlerRvizBackend"
},
"simulator": true,
"total_height": 300,
"joint_config": "TransformXYZDeck",
"simulate_rviz": true
}
},
{
"id": "deck",
"name": "deck",
"children": [
"tip_rack",
"plate_well",
"tube_rack",
"bottle_rack"
],
"parent": "liquid_handler",
"type": "deck",
"class": "TransformXYZDeck",
"position": {
"x": 0,
"y": 0,
"z": 18
},
"config": {
"type": "TransformXYZDeck",
"size_x": 624.3,
"size_y": 565.2,
"size_z": 900,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
}
},
"data": {}
},
{
"id": "tip_rack",
"name": "tip_rack",
"children": [
"tip_rack_A1"
],
"parent": "deck",
"type": "tip_rack",
"class": "tiprack_box",
"position": {
"x": 150,
"y": 7,
"z": 103
},
"config": {
"type": "TipRack",
"size_x": 134,
"size_y": 96,
"size_z": 7.0,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "tip_rack",
"model": "tiprack_box",
"ordering": [
"A1"
]
},
"data": {}
},
{
"id": "tip_rack_A1",
"name": "tip_rack_A1",
"children": [],
"parent": "tip_rack",
"type": "container",
"class": "",
"position": {
"x": 11.12,
"y": 75,
"z": -91.54
},
"config": {
"type": "TipSpot",
"size_x": 9,
"size_y": 9,
"size_z": 95,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "tip_spot",
"model": null,
"prototype_tip": {
"type": "Tip",
"total_tip_length": 95,
"has_filter": false,
"maximal_volume": 1000.0,
"fitting_depth": 3.29
}
},
"data": {
"tip": null,
"tip_state": null,
"pending_tip": null
}
},
{
"id": "plate_well",
"name": "plate_well",
"children": [
"plate_well_A1"
],
"parent": "deck",
"type": "plate",
"class": "plate_96",
"position": {
"x": 161,
"y": 116,
"z": 48.5
},
"config": {
"type": "Plate",
"size_x": 127.76,
"size_y": 85.48,
"size_z": 45.5,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "plate",
"model": "plate_96",
"ordering": [
"A1"
]
},
"data": {}
},
{
"id": "plate_well_A1",
"name": "plate_well_A1",
"children": [],
"parent": "plate_well",
"type": "device",
"class": "",
"position": {
"x": 10.1,
"y": 70,
"z": 6.1
},
"config": {
"type": "Well",
"size_x": 8.2,
"size_y": 8.2,
"size_z": 38,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "well",
"model": null,
"max_volume": 2000,
"material_z_thickness": null,
"compute_volume_from_height": null,
"compute_height_from_volume": null,
"bottom_type": "unknown",
"cross_section_type": "rectangle"
},
"data": {
"liquids": [["water", 50.0]],
"pending_liquids": [["water", 50.0]],
"liquid_history": []
}
},
{
"id": "tube_rack",
"name": "tube_rack",
"children": [
"tube_rack_A1"
],
"parent": "deck",
"type": "container",
"class": "tube_container",
"position": {
"x": 0,
"y": 127,
"z": 0
},
"config": {
"type": "Plate",
"size_x": 151,
"size_y": 75,
"size_z": 75,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"model": "tube_container",
"ordering": [
"A1"
]
},
"data": {}
},
{
"id": "tube_rack_A1",
"name": "tube_rack_A1",
"children": [],
"parent": "tube_rack",
"type": "device",
"class": "",
"position": {
"x": 6,
"y": 38,
"z": 10
},
"config": {
"type": "Well",
"size_x": 34,
"size_y": 34,
"size_z": 117,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "tube",
"model": null,
"max_volume": 2000,
"material_z_thickness": null,
"compute_volume_from_height": null,
"compute_height_from_volume": null,
"bottom_type": "unknown",
"cross_section_type": "rectangle"
},
"data": {
"liquids": [["water", 50.0]],
"pending_liquids": [["water", 50.0]],
"liquid_history": []
}
}
,
{
"id": "bottle_rack",
"name": "bottle_rack",
"children": [
"bottle_rack_A1"
],
"parent": "deck",
"type": "container",
"class": "bottle_container",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"type": "Plate",
"size_x": 130,
"size_y": 117,
"size_z": 8,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "tube_rack",
"model": "bottle_container",
"ordering": [
"A1"
]
},
"data": {}
},
{
"id": "bottle_rack_A1",
"name": "bottle_rack_A1",
"children": [],
"parent": "bottle_rack",
"type": "device",
"class": "",
"position": {
"x": 25,
"y": 18.5,
"z": 8
},
"config": {
"type": "Well",
"size_x": 80,
"size_y": 80,
"size_z": 117,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "tube",
"model": null,
"max_volume": 2000,
"material_z_thickness": null,
"compute_volume_from_height": null,
"compute_height_from_volume": null,
"bottom_type": "unknown",
"cross_section_type": "rectangle"
},
"data": {
"liquids": [["water", 50.0]],
"pending_liquids": [["water", 50.0]],
"liquid_history": []
}
}
],
"links": []
}

View File

@@ -3,7 +3,8 @@
"""
import asyncio
from typing import Dict, Any, Optional, List
from typing import Dict, Any, List
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
class SmartPumpController:
@@ -14,6 +15,8 @@ class SmartPumpController:
适用于实验室自动化系统中的液体处理任务。
"""
_ros_node: BaseROS2DeviceNode
def __init__(self, device_id: str = "smart_pump_01", port: str = "/dev/ttyUSB0"):
"""
初始化智能泵控制器
@@ -30,6 +33,9 @@ class SmartPumpController:
self.calibration_factor = 1.0
self.pump_mode = "continuous" # continuous, volume, rate
def post_init(self, ros_node: BaseROS2DeviceNode):
self._ros_node = ros_node
def connect_device(self, timeout: int = 10) -> bool:
"""
连接到泵设备
@@ -90,7 +96,7 @@ class SmartPumpController:
pump_time = (volume / flow_rate) * 60 # 转换为秒
self.current_flow_rate = flow_rate
await asyncio.sleep(min(pump_time, 3.0)) # 模拟泵送过程
await self._ros_node.sleep(min(pump_time, 3.0)) # 模拟泵送过程
self.total_volume_pumped += volume
self.current_flow_rate = 0.0
@@ -170,6 +176,8 @@ class AdvancedTemperatureController:
适用于需要精确温度控制的化学反应和材料处理过程。
"""
_ros_node: BaseROS2DeviceNode
def __init__(self, controller_id: str = "temp_controller_01"):
"""
初始化温度控制器
@@ -185,6 +193,9 @@ class AdvancedTemperatureController:
self.pid_enabled = True
self.temperature_history: List[Dict] = []
def post_init(self, ros_node: BaseROS2DeviceNode):
self._ros_node = ros_node
def set_target_temperature(self, temperature: float, rate: float = 10.0) -> bool:
"""
设置目标温度
@@ -238,7 +249,7 @@ class AdvancedTemperatureController:
}
)
await asyncio.sleep(step_time)
await self._ros_node.sleep(step_time)
# 保持历史记录不超过100条
if len(self.temperature_history) > 100:
@@ -330,6 +341,8 @@ class MultiChannelAnalyzer:
常用于光谱分析、电化学测量等应用场景。
"""
_ros_node: BaseROS2DeviceNode
def __init__(self, analyzer_id: str = "analyzer_01", channels: int = 8):
"""
初始化多通道分析仪
@@ -344,6 +357,9 @@ class MultiChannelAnalyzer:
self.is_measuring = False
self.sample_rate = 1000 # Hz
def post_init(self, ros_node: BaseROS2DeviceNode):
self._ros_node = ros_node
def configure_channel(self, channel: int, enabled: bool = True, unit: str = "V") -> bool:
"""
配置通道
@@ -376,7 +392,7 @@ class MultiChannelAnalyzer:
# 模拟数据采集
measurements = []
for second in range(duration):
for _ in range(duration):
timestamp = asyncio.get_event_loop().time()
frame_data = {}
@@ -391,7 +407,7 @@ class MultiChannelAnalyzer:
measurements.append({"timestamp": timestamp, "data": frame_data})
await asyncio.sleep(1.0) # 每秒采集一次
await self._ros_node.sleep(1.0) # 每秒采集一次
self.is_measuring = False
@@ -465,6 +481,8 @@ class AutomatedDispenser:
集成称重功能,确保分配精度和重现性。
"""
_ros_node: BaseROS2DeviceNode
def __init__(self, dispenser_id: str = "dispenser_01"):
"""
初始化自动分配器
@@ -479,6 +497,9 @@ class AutomatedDispenser:
self.container_capacity = 1000.0 # mL
self.precision_mode = True
def post_init(self, ros_node: BaseROS2DeviceNode):
self._ros_node = ros_node
def move_to_position(self, x: float, y: float, z: float) -> bool:
"""
移动到指定位置
@@ -517,7 +538,7 @@ class AutomatedDispenser:
if viscosity == "high":
dispense_time *= 2 # 高粘度液体需要更长时间
await asyncio.sleep(min(dispense_time, 5.0)) # 最多等待5秒
await self._ros_node.sleep(min(dispense_time, 5.0)) # 最多等待5秒
self.dispensed_total += volume

View File

@@ -0,0 +1,68 @@
import pytest
import json
import os
from pylabrobot.resources import Resource as ResourcePLR
from unilabos.resources.graphio import resource_bioyond_to_plr
from unilabos.ros.nodes.resource_tracker import ResourceTreeSet
from unilabos.registry.registry import lab_registry
from unilabos.resources.bioyond.decks import BIOYOND_PolymerReactionStation_Deck
lab_registry.setup()
type_mapping = {
"烧杯": ("BIOYOND_PolymerStation_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"),
"试剂瓶": ("BIOYOND_PolymerStation_1BottleCarrier", ""),
"样品板": ("BIOYOND_PolymerStation_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"),
"分装板": ("BIOYOND_PolymerStation_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"),
"样品瓶": ("BIOYOND_PolymerStation_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"),
"90%分装小瓶": ("BIOYOND_PolymerStation_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"),
"10%分装小瓶": ("BIOYOND_PolymerStation_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"),
}
@pytest.fixture
def bioyond_materials_reaction() -> list[dict]:
print("加载 BioYond 物料数据...")
print(os.getcwd())
with open("bioyond_materials_reaction.json", "r", encoding="utf-8") as f:
data = json.load(f)
print(f"加载了 {len(data)} 条物料数据")
return data
@pytest.fixture
def bioyond_materials_liquidhandling_1() -> list[dict]:
print("加载 BioYond 物料数据...")
print(os.getcwd())
with open("bioyond_materials_liquidhandling_1.json", "r", encoding="utf-8") as f:
data = json.load(f)
print(f"加载了 {len(data)} 条物料数据")
return data
@pytest.fixture
def bioyond_materials_liquidhandling_2() -> list[dict]:
print("加载 BioYond 物料数据...")
print(os.getcwd())
with open("bioyond_materials_liquidhandling_2.json", "r", encoding="utf-8") as f:
data = json.load(f)
print(f"加载了 {len(data)} 条物料数据")
return data
@pytest.mark.parametrize("materials_fixture", [
"bioyond_materials_reaction",
"bioyond_materials_liquidhandling_1",
])
def test_resourcetreeset_from_plr(materials_fixture, request) -> list[dict]:
materials = request.getfixturevalue(materials_fixture)
deck = BIOYOND_PolymerReactionStation_Deck("test_deck")
output = resource_bioyond_to_plr(materials, type_mapping=type_mapping, deck=deck)
print(deck.summary())
r = ResourceTreeSet.from_plr_resources([deck])
print(r.dump())
# json.dump(deck.serialize(), open("test.json", "w", encoding="utf-8"), indent=4)

View File

@@ -0,0 +1,186 @@
{
"workflow": [
{
"action": "transfer_liquid",
"action_args": {
"sources": "Liquid_1",
"targets": "Liquid_2",
"asp_vol": 66.0,
"dis_vol": 66.0,
"asp_flow_rate": 94.0,
"dis_flow_rate": 94.0
}
},
{
"action": "transfer_liquid",
"action_args": {
"sources": "Liquid_2",
"targets": "Liquid_3",
"asp_vol": 58.0,
"dis_vol": 96.0,
"asp_flow_rate": 94.0,
"dis_flow_rate": 94.0
}
},
{
"action": "transfer_liquid",
"action_args": {
"sources": "Liquid_4",
"targets": "Liquid_2",
"asp_vol": 85.0,
"dis_vol": 170.0,
"asp_flow_rate": 94.0,
"dis_flow_rate": 94.0
}
},
{
"action": "transfer_liquid",
"action_args": {
"sources": "Liquid_4",
"targets": "Liquid_2",
"asp_vol": 63.333333333333336,
"dis_vol": 170.0,
"asp_flow_rate": 94.0,
"dis_flow_rate": 94.0
}
},
{
"action": "transfer_liquid",
"action_args": {
"sources": "Liquid_2",
"targets": "Liquid_3",
"asp_vol": 72.0,
"dis_vol": 150.0,
"asp_flow_rate": 94.0,
"dis_flow_rate": 94.0
}
},
{
"action": "transfer_liquid",
"action_args": {
"sources": "Liquid_4",
"targets": "Liquid_2",
"asp_vol": 85.0,
"dis_vol": 170.0,
"asp_flow_rate": 94.0,
"dis_flow_rate": 94.0
}
},
{
"action": "transfer_liquid",
"action_args": {
"sources": "Liquid_4",
"targets": "Liquid_2",
"asp_vol": 63.333333333333336,
"dis_vol": 170.0,
"asp_flow_rate": 94.0,
"dis_flow_rate": 94.0
}
},
{
"action": "transfer_liquid",
"action_args": {
"sources": "Liquid_2",
"targets": "Liquid_3",
"asp_vol": 72.0,
"dis_vol": 150.0,
"asp_flow_rate": 94.0,
"dis_flow_rate": 94.0
}
},
{
"action": "transfer_liquid",
"action_args": {
"sources": "Liquid_2",
"targets": "Liquid_3",
"asp_vol": 20.0,
"dis_vol": 20.0,
"asp_flow_rate": 7.6,
"dis_flow_rate": 7.6
}
},
{
"action": "transfer_liquid",
"action_args": {
"sources": "Liquid_5",
"targets": "Liquid_2",
"asp_vol": 6.0,
"dis_vol": 12.0,
"asp_flow_rate": 7.6,
"dis_flow_rate": 7.6
}
},
{
"action": "transfer_liquid",
"action_args": {
"sources": "Liquid_5",
"targets": "Liquid_2",
"asp_vol": 10.666666666666666,
"dis_vol": 12.0,
"asp_flow_rate": 7.599999999999999,
"dis_flow_rate": 7.6
}
},
{
"action": "transfer_liquid",
"action_args": {
"sources": "Liquid_2",
"targets": "Liquid_6",
"asp_vol": 12.0,
"dis_vol": 10.0,
"asp_flow_rate": 7.6,
"dis_flow_rate": 7.6
}
}
],
"reagent": {
"Liquid_6": {
"slot": 1,
"well": [
"A2"
],
"labware": "elution plate"
},
"Liquid_1": {
"slot": 2,
"well": [
"A1",
"A2",
"A4"
],
"labware": "reagent reservoir"
},
"Liquid_4": {
"slot": 2,
"well": [
"A1",
"A2",
"A4"
],
"labware": "reagent reservoir"
},
"Liquid_5": {
"slot": 2,
"well": [
"A1",
"A2",
"A4"
],
"labware": "reagent reservoir"
},
"Liquid_2": {
"slot": 4,
"well": [
"A2"
],
"labware": "TAG1 plate on Magnetic Module GEN2"
},
"Liquid_3": {
"slot": 12,
"well": [
"A1"
],
"labware": "Opentrons Fixed Trash"
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

View File

@@ -0,0 +1,63 @@
{
"steps_info": [
{
"step_number": 1,
"action": "transfer_liquid",
"parameters": {
"source": "sample supernatant",
"target": "antibody-coated well",
"volume": 100
}
},
{
"step_number": 2,
"action": "transfer_liquid",
"parameters": {
"source": "washing buffer",
"target": "antibody-coated well",
"volume": 200
}
},
{
"step_number": 3,
"action": "transfer_liquid",
"parameters": {
"source": "washing buffer",
"target": "antibody-coated well",
"volume": 200
}
},
{
"step_number": 4,
"action": "transfer_liquid",
"parameters": {
"source": "washing buffer",
"target": "antibody-coated well",
"volume": 200
}
},
{
"step_number": 5,
"action": "transfer_liquid",
"parameters": {
"source": "TMB substrate",
"target": "antibody-coated well",
"volume": 100
}
}
],
"labware_info": [
{"reagent_name": "sample supernatant", "material_name": "96深孔板", "positions": 1},
{"reagent_name": "washing buffer", "material_name": "储液槽", "positions": 2},
{"reagent_name": "TMB substrate", "material_name": "储液槽", "positions": 3},
{"reagent_name": "antibody-coated well", "material_name": "96 细胞培养皿", "positions": 4},
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 5},
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 6},
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 7},
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 8},
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 9},
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 10},
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 11},
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 13}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

View File

@@ -0,0 +1,94 @@
import json
import sys
from datetime import datetime
from pathlib import Path
ROOT_DIR = Path(__file__).resolve().parents[2]
if str(ROOT_DIR) not in sys.path:
sys.path.insert(0, str(ROOT_DIR))
import pytest
from scripts.workflow import build_protocol_graph, draw_protocol_graph, draw_protocol_graph_with_ports
ROOT_DIR = Path(__file__).resolve().parents[2]
if str(ROOT_DIR) not in sys.path:
sys.path.insert(0, str(ROOT_DIR))
def _normalize_steps(data):
normalized = []
for step in data:
action = step.get("action") or step.get("operation")
if not action:
continue
raw_params = step.get("parameters") or step.get("action_args") or {}
params = dict(raw_params)
if "source" in raw_params and "sources" not in raw_params:
params["sources"] = raw_params["source"]
if "target" in raw_params and "targets" not in raw_params:
params["targets"] = raw_params["target"]
description = step.get("description") or step.get("purpose")
step_dict = {"action": action, "parameters": params}
if description:
step_dict["description"] = description
normalized.append(step_dict)
return normalized
def _normalize_labware(data):
labware = {}
for item in data:
reagent_name = item.get("reagent_name")
key = reagent_name or item.get("material_name") or item.get("name")
if not key:
continue
key = str(key)
idx = 1
original_key = key
while key in labware:
idx += 1
key = f"{original_key}_{idx}"
labware[key] = {
"slot": item.get("positions") or item.get("slot"),
"labware": item.get("material_name") or item.get("labware"),
"well": item.get("well", []),
"type": item.get("type", "reagent"),
"role": item.get("role", ""),
"name": key,
}
return labware
@pytest.mark.parametrize("protocol_name", [
"example_bio",
# "bioyond_materials_liquidhandling_1",
"example_prcxi",
])
def test_build_protocol_graph(protocol_name):
data_path = Path(__file__).with_name(f"{protocol_name}.json")
with data_path.open("r", encoding="utf-8") as fp:
d = json.load(fp)
if "workflow" in d and "reagent" in d:
protocol_steps = d["workflow"]
labware_info = d["reagent"]
elif "steps_info" in d and "labware_info" in d:
protocol_steps = _normalize_steps(d["steps_info"])
labware_info = _normalize_labware(d["labware_info"])
else:
raise ValueError("Unsupported protocol format")
graph = build_protocol_graph(
labware_info=labware_info,
protocol_steps=protocol_steps,
workstation_name="PRCXi",
)
timestamp = datetime.now().strftime("%Y%m%d_%H%M")
output_path = data_path.with_name(f"{protocol_name}_graph_{timestamp}.png")
draw_protocol_graph_with_ports(graph, str(output_path))
print(graph)

View File

@@ -1 +1 @@
__version__ = "0.10.7"
__version__ = "0.10.10"

View File

@@ -13,7 +13,7 @@ def start_backend(
graph=None,
controllers_config: dict = {},
bridges=[],
without_host: bool = False,
is_slave: bool = False,
visual: str = "None",
resources_mesh_config: dict = {},
**kwargs,
@@ -32,7 +32,7 @@ def start_backend(
raise ValueError(f"Unsupported backend: {backend}")
backend_thread = threading.Thread(
target=main if not without_host else slave,
target=main if not is_slave else slave,
args=(
devices_config,
resources_config,

View File

@@ -375,22 +375,23 @@ def main():
args_dict["bridges"] = []
# 获取通信客户端仅支持WebSocket
comm_client = get_communication_client()
if "websocket" in args_dict["app_bridges"]:
args_dict["bridges"].append(comm_client)
if "fastapi" in args_dict["app_bridges"]:
args_dict["bridges"].append(http_client)
if "websocket" in args_dict["app_bridges"]:
# 获取通信客户端仅支持WebSocket
if BasicConfig.is_host_mode:
comm_client = get_communication_client()
if "websocket" in args_dict["app_bridges"]:
args_dict["bridges"].append(comm_client)
def _exit(signum, frame):
comm_client.stop()
sys.exit(0)
def _exit(signum, frame):
comm_client.stop()
sys.exit(0)
signal.signal(signal.SIGINT, _exit)
signal.signal(signal.SIGTERM, _exit)
comm_client.start()
else:
print_status("SlaveMode跳过Websocket连接")
signal.signal(signal.SIGINT, _exit)
signal.signal(signal.SIGTERM, _exit)
comm_client.start()
args_dict["resources_mesh_config"] = {}
args_dict["resources_edge_config"] = resource_edge_info
# web visiualize 2D
@@ -418,7 +419,23 @@ def main():
)
server_thread.start()
asyncio.set_event_loop(asyncio.new_event_loop())
resource_visualization.start()
try:
resource_visualization.start()
except OSError as e:
if "AMENT_PREFIX_PATH" in str(e):
print_status(
f"ROS 2环境未正确设置跳过3D可视化启动。错误详情: {e}",
"warning"
)
print_status(
"建议解决方案:\n"
"1. 激活Conda环境: conda activate unilab\n"
"2. 或使用 --backend simple 参数\n"
"3. 或使用 --visual disable 参数禁用可视化",
"info"
)
else:
raise
while True:
time.sleep(1)
else:

View File

@@ -1,11 +1,12 @@
import json
import time
from typing import Optional, Tuple, Dict, Any
from unilabos.utils.log import logger
from unilabos.utils.type_check import TypeEncoder
def register_devices_and_resources(lab_registry):
def register_devices_and_resources(lab_registry, gather_only=False) -> Optional[Tuple[Dict[str, Any], Dict[str, Any]]]:
"""
注册设备和资源到服务器仅支持HTTP
"""
@@ -28,6 +29,8 @@ def register_devices_and_resources(lab_registry):
resources_to_register[resource_info["id"]] = resource_info
logger.debug(f"[UniLab Register] 收集资源: {resource_info['id']}")
if gather_only:
return devices_to_register, resources_to_register
# 注册设备
if devices_to_register:
try:

View File

@@ -6,6 +6,8 @@ HTTP客户端模块
import json
import os
import time
from threading import Thread
from typing import List, Dict, Any, Optional
import requests
@@ -84,14 +86,14 @@ class HTTPClient:
f"{self.remote_addr}/edge/material",
json={"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid},
headers={"Authorization": f"Lab {self.auth}"},
timeout=100,
timeout=60,
)
else:
response = requests.put(
f"{self.remote_addr}/edge/material",
json={"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid},
headers={"Authorization": f"Lab {self.auth}"},
timeout=100,
timeout=10,
)
with open(os.path.join(BasicConfig.working_dir, "res_resource_tree_add.json"), "w", encoding="utf-8") as f:
@@ -126,12 +128,16 @@ class HTTPClient:
Returns:
Dict[str, str]: 旧UUID到新UUID的映射关系 {old_uuid: new_uuid}
"""
with open(os.path.join(BasicConfig.working_dir, "req_resource_tree_get.json"), "w", encoding="utf-8") as f:
f.write(json.dumps({"uuids": uuid_list, "with_children": with_children}, indent=4))
response = requests.post(
f"{self.remote_addr}/edge/material/query",
json={"uuids": uuid_list, "with_children": with_children},
headers={"Authorization": f"Lab {self.auth}"},
timeout=100,
)
with open(os.path.join(BasicConfig.working_dir, "res_resource_tree_get.json"), "w", encoding="utf-8") as f:
f.write(f"{response.status_code}" + "\n" + response.text)
if response.status_code == 200:
res = response.json()
if "code" in res and res["code"] != 0:
@@ -187,12 +193,16 @@ class HTTPClient:
Returns:
Dict: 返回的资源数据
"""
with open(os.path.join(BasicConfig.working_dir, "req_resource_get.json"), "w", encoding="utf-8") as f:
f.write(json.dumps({"id": id, "with_children": with_children}, indent=4))
response = requests.get(
f"{self.remote_addr}/lab/material",
params={"id": id, "with_children": with_children},
headers={"Authorization": f"Lab {self.auth}"},
timeout=20,
)
with open(os.path.join(BasicConfig.working_dir, "res_resource_get.json"), "w", encoding="utf-8") as f:
f.write(f"{response.status_code}" + "\n" + response.text)
return response.json()
def resource_del(self, id: str) -> requests.Response:

View File

@@ -261,29 +261,28 @@ class DeviceActionManager:
device_key = job_info.device_action_key
# 如果是正在执行的任务
if (
device_key in self.active_jobs and self.active_jobs[device_key].job_id == job_id
): # 后面需要和cancel_goal进行联动而不是在这里进行处理现在默认等待这个job结束
# del self.active_jobs[device_key]
# job_info.status = JobStatus.ENDED
# # 从all_jobs中移除
# del self.all_jobs[job_id]
# job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
# logger.info(f"[DeviceActionManager] Active job {job_log} cancelled for {device_key}")
if device_key in self.active_jobs and self.active_jobs[device_key].job_id == job_id:
# 清理active job状态
del self.active_jobs[device_key]
job_info.status = JobStatus.ENDED
# 从all_jobs中移除
del self.all_jobs[job_id]
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
logger.info(f"[DeviceActionManager] Active job {job_log} cancelled for {device_key}")
# # 启动下一个任务
# if device_key in self.device_queues and self.device_queues[device_key]:
# next_job = self.device_queues[device_key].pop(0)
# # 将下一个job设置为READY状态并放入active_jobs
# next_job.status = JobStatus.READY
# next_job.update_timestamp()
# next_job.set_ready_timeout(10)
# self.active_jobs[device_key] = next_job
# next_job_log = format_job_log(next_job.job_id, next_job.task_id,
# next_job.device_id, next_job.action_name)
# logger.info(f"[DeviceActionManager] Next job {next_job_log} can start after cancel")
# return True
pass
# 启动下一个任务
if device_key in self.device_queues and self.device_queues[device_key]:
next_job = self.device_queues[device_key].pop(0)
# 将下一个job设置为READY状态并放入active_jobs
next_job.status = JobStatus.READY
next_job.update_timestamp()
next_job.set_ready_timeout(10)
self.active_jobs[device_key] = next_job
next_job_log = format_job_log(
next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name
)
logger.info(f"[DeviceActionManager] Next job {next_job_log} can start after cancel")
return True
# 如果是排队中的任务
elif device_key in self.device_queues:
@@ -741,31 +740,51 @@ class MessageProcessor:
job_info.action_name if job_info else "",
)
# 按job_id取消单个job
# 先通知HostNode取消ROS2 action如果存在
host_node = HostNode.get_instance(0)
ros_cancel_success = False
if host_node:
ros_cancel_success = host_node.cancel_goal(job_id)
if ros_cancel_success:
logger.info(f"[MessageProcessor] ROS2 cancel request sent for job {job_log}")
else:
logger.debug(
f"[MessageProcessor] Job {job_log} not in ROS2 goals " "(may be queued or already finished)"
)
# 按job_id取消单个job清理状态机
success = self.device_manager.cancel_job(job_id)
if success:
# 通知HostNode取消
host_node = HostNode.get_instance(0)
if host_node:
host_node.cancel_goal(job_id)
logger.info(f"[MessageProcessor] Job {job_log} cancelled")
logger.info(f"[MessageProcessor] Job {job_log} cancelled from queue/active list")
# 通知QueueProcessor有队列更新
if self.queue_processor:
self.queue_processor.notify_queue_update()
else:
logger.warning(f"[MessageProcessor] Failed to cancel job {job_log}")
logger.warning(f"[MessageProcessor] Failed to cancel job {job_log} from queue")
elif task_id:
# 按task_id取消所有相关job
# 先通知HostNode取消所有ROS2 actions
# 需要先获取所有相关job_ids
jobs_to_cancel = []
with self.device_manager.lock:
jobs_to_cancel = [
job_info for job_info in self.device_manager.all_jobs.values() if job_info.task_id == task_id
]
host_node = HostNode.get_instance(0)
if host_node and jobs_to_cancel:
ros_cancelled_count = 0
for job_info in jobs_to_cancel:
if host_node.cancel_goal(job_info.job_id):
ros_cancelled_count += 1
logger.info(
f"[MessageProcessor] Sent ROS2 cancel for " f"{ros_cancelled_count}/{len(jobs_to_cancel)} jobs"
)
# 按task_id取消所有相关job清理状态机
cancelled_job_ids = self.device_manager.cancel_jobs_by_task_id(task_id)
if cancelled_job_ids:
# 通知HostNode取消所有job
host_node = HostNode.get_instance(0)
if host_node:
for cancelled_job_id in cancelled_job_ids:
host_node.cancel_goal(cancelled_job_id)
logger.info(f"[MessageProcessor] Cancelled {len(cancelled_job_ids)} jobs for task_id: {task_id}")
# 通知QueueProcessor有队列更新
@@ -1056,11 +1075,19 @@ class QueueProcessor:
"""处理任务完成"""
# 获取job信息用于日志
job_info = self.device_manager.get_job_info(job_id)
# 如果job不存在说明可能已被手动取消
if not job_info:
logger.debug(
f"[QueueProcessor] Job {job_id[:8]} not found in manager " "(may have been cancelled manually)"
)
return
job_log = format_job_log(
job_id,
job_info.task_id if job_info else "",
job_info.device_id if job_info else "",
job_info.action_name if job_info else "",
job_info.task_id,
job_info.device_id,
job_info.action_name,
)
logger.info(f"[QueueProcessor] Job {job_log} completed with status: {status}")

View File

@@ -0,0 +1,19 @@
# OPC UA 通用客户端
本模块提供了一个通用的 OPC UA 客户端实现可以通过外部配置CSV文件来定义节点并通过JSON配置来执行工作流。
## 特点
- 支持通过 CSV 文件配置 OPC UA 节点(只需提供名称、类型和数据类型,支持节点为中文名需指定NodeLanguage
- 自动查找服务器中的节点无需知道确切的节点ID
- 提供工作流机制
- 支持通过 JSON 配置创建工作流
## 使用方法
step1: 准备opcua_nodes.csv文件
step2: 编写opcua_workflow_example.json,以定义工作流。指定opcua_nodes.csv
step3: 编写工作流对应action
step4: 编写opcua_example.yaml注册表
step5: 编写opcua_example.json组态图。指定opcua_workflow_example.json定义工作流文件

View File

@@ -0,0 +1,9 @@
from unilabos.device_comms.opcua_client.node.uniopcua import Variable, Method, Object, NodeType, DataType
__all__ = [
'Variable',
'Method',
'Object',
'NodeType',
'DataType',
]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
from unilabos.device_comms.opcua_client.node.uniopcua import Variable, Method, Object, NodeType, DataType, Base
__all__ = [
'Variable',
'Method',
'Object',
'NodeType',
'DataType',
'Base',
]

View File

@@ -0,0 +1,180 @@
# coding=utf-8
from enum import Enum
from abc import ABC, abstractmethod
from typing import Tuple, Union, Optional, Any, List
from opcua import Client, Node
from opcua.ua import NodeId, NodeClass, VariantType
class DataType(Enum):
BOOLEAN = VariantType.Boolean
SBYTE = VariantType.SByte
BYTE = VariantType.Byte
INT16 = VariantType.Int16
UINT16 = VariantType.UInt16
INT32 = VariantType.Int32
UINT32 = VariantType.UInt32
INT64 = VariantType.Int64
UINT64 = VariantType.UInt64
FLOAT = VariantType.Float
DOUBLE = VariantType.Double
STRING = VariantType.String
DATETIME = VariantType.DateTime
BYTESTRING = VariantType.ByteString
class NodeType(Enum):
VARIABLE = NodeClass.Variable
OBJECT = NodeClass.Object
METHOD = NodeClass.Method
OBJECTTYPE = NodeClass.ObjectType
VARIABLETYPE = NodeClass.VariableType
REFERENCETYPE = NodeClass.ReferenceType
DATATYPE = NodeClass.DataType
VIEW = NodeClass.View
class Base(ABC):
def __init__(self, client: Client, name: str, node_id: str, typ: NodeType, data_type: DataType):
self._node_id: str = node_id
self._client = client
self._name = name
self._type = typ
self._data_type = data_type
self._node: Optional[Node] = None
def _get_node(self) -> Node:
if self._node is None:
try:
# 检查是否是NumericNodeId(ns=X;i=Y)格式
if "NumericNodeId" in self._node_id:
# 从字符串中提取命名空间和标识符
import re
match = re.search(r'ns=(\d+);i=(\d+)', self._node_id)
if match:
ns = int(match.group(1))
identifier = int(match.group(2))
node_id = NodeId(identifier, ns)
self._node = self._client.get_node(node_id)
else:
raise ValueError(f"无法解析节点ID: {self._node_id}")
else:
# 直接使用节点ID字符串
self._node = self._client.get_node(self._node_id)
except Exception as e:
print(f"获取节点失败: {self._node_id}, 错误: {e}")
raise
return self._node
@abstractmethod
def read(self) -> Tuple[Any, bool]:
"""读取节点值,返回(值, 是否出错)"""
pass
@abstractmethod
def write(self, value: Any) -> bool:
"""写入节点值,返回是否出错"""
pass
@property
def type(self) -> NodeType:
return self._type
@property
def node_id(self) -> str:
return self._node_id
@property
def name(self) -> str:
return self._name
class Variable(Base):
def __init__(self, client: Client, name: str, node_id: str, data_type: DataType):
super().__init__(client, name, node_id, NodeType.VARIABLE, data_type)
def read(self) -> Tuple[Any, bool]:
try:
value = self._get_node().get_value()
return value, False
except Exception as e:
print(f"读取变量 {self._name} 失败: {e}")
return None, True
def write(self, value: Any) -> bool:
try:
self._get_node().set_value(value)
return False
except Exception as e:
print(f"写入变量 {self._name} 失败: {e}")
return True
class Method(Base):
def __init__(self, client: Client, name: str, node_id: str, parent_node_id: str, data_type: DataType):
super().__init__(client, name, node_id, NodeType.METHOD, data_type)
self._parent_node_id = parent_node_id
self._parent_node = None
def _get_parent_node(self) -> Node:
if self._parent_node is None:
try:
# 检查是否是NumericNodeId(ns=X;i=Y)格式
if "NumericNodeId" in self._parent_node_id:
# 从字符串中提取命名空间和标识符
import re
match = re.search(r'ns=(\d+);i=(\d+)', self._parent_node_id)
if match:
ns = int(match.group(1))
identifier = int(match.group(2))
node_id = NodeId(identifier, ns)
self._parent_node = self._client.get_node(node_id)
else:
raise ValueError(f"无法解析父节点ID: {self._parent_node_id}")
else:
# 直接使用节点ID字符串
self._parent_node = self._client.get_node(self._parent_node_id)
except Exception as e:
print(f"获取父节点失败: {self._parent_node_id}, 错误: {e}")
raise
return self._parent_node
def read(self) -> Tuple[Any, bool]:
"""方法节点不支持读取操作"""
return None, True
def write(self, value: Any) -> bool:
"""方法节点不支持写入操作"""
return True
def call(self, *args) -> Tuple[Any, bool]:
"""调用方法,返回(返回值, 是否出错)"""
try:
result = self._get_parent_node().call_method(self._get_node(), *args)
return result, False
except Exception as e:
print(f"调用方法 {self._name} 失败: {e}")
return None, True
class Object(Base):
def __init__(self, client: Client, name: str, node_id: str):
super().__init__(client, name, node_id, NodeType.OBJECT, None)
def read(self) -> Tuple[Any, bool]:
"""对象节点不支持直接读取操作"""
return None, True
def write(self, value: Any) -> bool:
"""对象节点不支持直接写入操作"""
return True
def get_children(self) -> Tuple[List[Node], bool]:
"""获取子节点列表,返回(子节点列表, 是否出错)"""
try:
children = self._get_node().get_children()
return children, False
except Exception as e:
print(f"获取对象 {self._name} 的子节点失败: {e}")
return [], True

View File

@@ -0,0 +1,98 @@
{
"register_node_list_from_csv_path": {
"path": "simple_opcua_nodes.csv"
},
"create_flow": [
{
"name": "温度控制流程",
"action": [
{
"name": "温度控制动作",
"node_function_to_create": [
{
"func_name": "read_temperature",
"node_name": "Temperature",
"mode": "read"
},
{
"func_name": "read_heating_status",
"node_name": "HeatingStatus",
"mode": "read"
},
{
"func_name": "set_heating",
"node_name": "HeatingEnabled",
"mode": "write",
"value": true
}
],
"create_init_function": {
"func_name": "init_setpoint",
"node_name": "Setpoint",
"mode": "write",
"value": 25.0
},
"create_start_function": {
"func_name": "start_heating_control",
"node_name": "HeatingEnabled",
"mode": "write",
"write_functions": [
"set_heating"
],
"condition_functions": [
"read_temperature",
"read_heating_status"
],
"stop_condition_expression": "read_temperature >= 25.0 and read_heating_status"
},
"create_stop_function": {
"func_name": "stop_heating",
"node_name": "HeatingEnabled",
"mode": "write",
"value": false
},
"create_cleanup_function": null
}
]
},
{
"name": "报警重置流程",
"action": [
{
"name": "报警重置动作",
"node_function_to_create": [
{
"func_name": "reset_alarm",
"node_name": "ResetAlarm",
"mode": "call",
"value": []
}
],
"create_init_function": null,
"create_start_function": {
"func_name": "start_reset_alarm",
"node_name": "ResetAlarm",
"mode": "call",
"write_functions": [],
"condition_functions": [
"reset_alarm"
],
"stop_condition_expression": "True"
},
"create_stop_function": null,
"create_cleanup_function": null
}
]
},
{
"name": "完整控制流程",
"action": [
"温度控制流程",
"报警重置流程"
]
}
],
"execute_flow": [
"完整控制流程"
]
}

View File

@@ -0,0 +1,2 @@
Name,EnglishName,NodeType,DataType,NodeLanguage
中文名,EnglishName,VARIABLE,INT32,Chinese
1 Name EnglishName NodeType DataType NodeLanguage
2 中文名 EnglishName VARIABLE INT32 Chinese

View File

@@ -0,0 +1,30 @@
{
"register_node_list_from_csv_path": {
"path": "opcua_nodes_example.csv"
},
"create_flow": [
{
"name": "name",
"description": "description",
"parameters": ["parameter1", "parameter2"],
"action": [
{
"init_function": {
"func_name": "init_grab_params",
"write_nodes": ["parameter1", "parameter2"]
},
"start_function": {
"func_name": "start_grab",
"write_nodes": {"parameter_start": true},
"condition_nodes": ["parameter_condition"],
"stop_condition_expression": "parameter_condition == True"
},
"stop_function": {
"func_name": "stop_grab",
"write_nodes": {"parameter_start": false}
}
}
]
}
]
}

View File

@@ -0,0 +1,311 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
OPC UA测试服务器
用于测试OPC UA客户端功能特别是temperature_control和valve_control工作流
"""
import sys
import time
import logging
from opcua import Server, ua
import threading
# 设置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class OpcUaTestServer:
"""OPC UA测试服务器类"""
def __init__(self, endpoint="opc.tcp://localhost:4840/freeopcua/server/"):
"""
初始化OPC UA服务器
Args:
endpoint: 服务器端点URL
"""
self.server = Server()
self.server.set_endpoint(endpoint)
# 设置服务器名称
self.server.set_server_name("UniLabOS OPC UA Test Server")
# 设置服务器命名空间
self.idx = self.server.register_namespace("http://unilabos.com/opcua/test")
# 获取Objects节点
self.objects = self.server.get_objects_node()
# 创建设备对象
self.device = self.objects.add_object(self.idx, "TestDevice")
# 存储所有节点的字典
self.nodes = {}
# 初始化标志
self.running = False
# 控制标志
self.simulation_active = True
def add_variable(self, name, value, data_type=None):
"""
添加变量节点
Args:
name: 变量名称
value: 初始值
data_type: 数据类型 (可选)
"""
if data_type is None:
var = self.device.add_variable(self.idx, name, value)
else:
var = self.device.add_variable(self.idx, name, value, data_type)
# 设置变量可写
var.set_writable()
# 存储节点
self.nodes[name] = var
logger.info(f"添加变量节点: {name}, 初始值: {value}")
return var
def add_method(self, name, callback, inputs=None, outputs=None):
"""
添加方法节点
Args:
name: 方法名称
callback: 回调函数
inputs: 输入参数列表 [(name, type), ...]
outputs: 输出参数列表 [(name, type), ...]
"""
if inputs is None:
inputs = []
if outputs is None:
outputs = []
# 创建输入参数
input_args = []
for arg_name, arg_type in inputs:
input_args.append(ua.Argument())
input_args[-1].Name = arg_name
input_args[-1].DataType = arg_type
input_args[-1].ValueRank = -1
# 创建输出参数
output_args = []
for arg_name, arg_type in outputs:
output_args.append(ua.Argument())
output_args[-1].Name = arg_name
output_args[-1].DataType = arg_type
output_args[-1].ValueRank = -1
# 添加方法
method = self.device.add_method(
self.idx,
name,
callback,
input_args,
output_args
)
# 存储节点
self.nodes[name] = method
logger.info(f"添加方法节点: {name}")
return method
def start(self):
"""启动服务器"""
if not self.running:
self.server.start()
self.running = True
logger.info("OPC UA服务器已启动")
# 启动模拟线程
self.simulation_thread = threading.Thread(target=self.run_simulation)
self.simulation_thread.daemon = True
self.simulation_thread.start()
def stop(self):
"""停止服务器"""
if self.running:
self.simulation_active = False
if hasattr(self, 'simulation_thread'):
self.simulation_thread.join(timeout=2)
self.server.stop()
self.running = False
logger.info("OPC UA服务器已停止")
def get_node(self, name):
"""获取节点"""
if name in self.nodes:
return self.nodes[name]
return None
def update_variable(self, name, value):
"""更新变量值"""
if name in self.nodes:
self.nodes[name].set_value(value)
logger.debug(f"更新变量 {name} = {value}")
return True
logger.warning(f"变量 {name} 不存在")
return False
def run_simulation(self):
"""运行模拟线程"""
logger.info("启动模拟线程")
temp = 20.0
valve_position = 0.0
flow_rate = 0.0
while self.simulation_active and self.running:
try:
# 温度控制模拟
heating_enabled = self.get_node("HeatingEnabled").get_value()
setpoint = self.get_node("Setpoint").get_value()
if heating_enabled:
self.update_variable("HeatingStatus", True)
if temp < setpoint:
temp += 0.5 # 加快温度上升速度
else:
temp -= 0.1
else:
self.update_variable("HeatingStatus", False)
if temp > 20.0:
temp -= 0.2
# 更新温度
self.update_variable("Temperature", round(temp, 2))
# 阀门控制模拟
valve_control = self.get_node("ValveControl").get_value()
valve_setpoint = self.get_node("ValveSetpoint").get_value()
if valve_control:
if valve_position < valve_setpoint:
valve_position += 5.0 # 加快阀门开启速度
if valve_position > valve_setpoint:
valve_position = valve_setpoint
else:
valve_position -= 1.0
if valve_position < 0:
valve_position = 0
else:
if valve_position > 0:
valve_position -= 5.0
if valve_position < 0:
valve_position = 0
# 更新阀门位置
self.update_variable("ValvePosition", round(valve_position, 2))
# 流量模拟 - 与阀门位置成正比
flow_rate = valve_position * 0.2 # 简单线性关系
self.update_variable("FlowRate", round(flow_rate, 2))
# 更新系统状态
status = []
if heating_enabled:
status.append("Heating")
if valve_control:
status.append("Valve_Open")
if status:
self.update_variable("SystemStatus", "_".join(status))
else:
self.update_variable("SystemStatus", "Idle")
# 每200毫秒更新一次
time.sleep(0.2)
except Exception as e:
logger.error(f"模拟线程错误: {e}")
time.sleep(1) # 出错时稍等一会再继续
logger.info("模拟线程已停止")
def reset_alarm_callback(parent, *args):
"""重置报警的回调函数"""
logger.info("调用了重置报警方法")
return True
def start_process_callback(parent, *args):
"""启动流程的回调函数"""
process_id = args[0] if args else 0
logger.info(f"启动流程 ID: {process_id}")
return process_id
def stop_process_callback(parent, *args):
"""停止流程的回调函数"""
process_id = args[0] if args else 0
logger.info(f"停止流程 ID: {process_id}")
return True
def main():
"""主函数"""
try:
# 创建服务器
server = OpcUaTestServer()
# 添加变量节点 - 温度控制相关
server.add_variable("Temperature", 20.0, ua.VariantType.Float)
server.add_variable("Setpoint", 22.0, ua.VariantType.Float)
server.add_variable("HeatingEnabled", False, ua.VariantType.Boolean)
server.add_variable("HeatingStatus", False, ua.VariantType.Boolean)
# 添加变量节点 - 阀门控制相关
server.add_variable("ValvePosition", 0.0, ua.VariantType.Float)
server.add_variable("ValveSetpoint", 0.0, ua.VariantType.Float)
server.add_variable("ValveControl", False, ua.VariantType.Boolean)
server.add_variable("FlowRate", 0.0, ua.VariantType.Float)
# 其他状态变量
server.add_variable("SystemStatus", "Idle", ua.VariantType.String)
# 添加方法节点
server.add_method(
"ResetAlarm",
reset_alarm_callback,
[],
[("Result", ua.VariantType.Boolean)]
)
server.add_method(
"StartProcess",
start_process_callback,
[("ProcessId", ua.VariantType.Int32)],
[("Result", ua.VariantType.Int32)]
)
server.add_method(
"StopProcess",
stop_process_callback,
[("ProcessId", ua.VariantType.Int32)],
[("Result", ua.VariantType.Boolean)]
)
# 启动服务器
server.start()
logger.info("服务器已启动按Ctrl+C停止")
# 保持服务器运行
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
logger.info("收到键盘中断,正在停止服务器...")
# 停止服务器
server.stop()
except Exception as e:
logger.error(f"服务器错误: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,25 @@
dummy2_robot:
kinematics:
# DH parameters for Dummy2 6-DOF robot arm
# [theta, d, a, alpha] for each joint
joint_1: [0.0, 0.1, 0.0, 1.5708] # Base rotation
joint_2: [0.0, 0.0, 0.2, 0.0] # Shoulder
joint_3: [0.0, 0.0, 0.15, 0.0] # Elbow
joint_4: [0.0, 0.1, 0.0, 1.5708] # Wrist roll
joint_5: [0.0, 0.0, 0.0, -1.5708] # Wrist pitch
joint_6: [0.0, 0.06, 0.0, 0.0] # Wrist yaw
# Tool center point offset from last joint
tcp_offset:
x: 0.0
y: 0.0
z: 0.04
# Workspace limits
workspace:
x_min: -0.5
x_max: 0.5
y_min: -0.5
y_max: 0.5
z_min: 0.0
z_max: 0.6

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--This does not replace URDF, and is not an extension of URDF.
This is a format for representing semantic information about the robot structure.
A URDF file must exist for this robot as well, where the joints and the links that are referenced are defined
-->
<robot name="dummy2">
<!--GROUPS: Representation of a set of joints and links. This can be useful for specifying DOF to plan for, defining arms, end effectors, etc-->
<!--LINKS: When a link is specified, the parent joint of that link (if it exists) is automatically included-->
<!--JOINTS: When a joint is specified, the child link of that joint (which will always exist) is automatically included-->
<!--CHAINS: When a chain is specified, all the links along the chain (including endpoints) are included in the group. Additionally, all the joints that are parents to included links are also included. This means that joints along the chain and the parent joint of the base link are included in the group-->
<!--SUBGROUPS: Groups can also be formed by referencing to already defined group names-->
<group name="dummy2_arm">
<joint name="virtual_joint"/>
<joint name="Joint1"/>
<joint name="Joint2"/>
<joint name="Joint3"/>
<joint name="Joint4"/>
<joint name="Joint5"/>
<joint name="Joint6"/>
</group>
<!--GROUP STATES: Purpose: Define a named state for a particular group, in terms of joint values. This is useful to define states like 'folded arms'-->
<group_state name="home" group="dummy2_arm">
<joint name="Joint1" value="0"/>
<joint name="Joint2" value="0"/>
<joint name="Joint3" value="0"/>
<joint name="Joint4" value="0"/>
<joint name="Joint5" value="0"/>
<joint name="Joint6" value="0"/>
</group_state>
<!--VIRTUAL JOINT: Purpose: this element defines a virtual joint between a robot link and an external frame of reference (considered fixed with respect to the robot)-->
<virtual_joint name="virtual_joint" type="fixed" parent_frame="world" child_link="base_link"/>
<!--DISABLE COLLISIONS: By default it is assumed that any link of the robot could potentially come into collision with any other link in the robot. This tag disables collision checking between a specified pair of links. -->
<disable_collisions link1="J1_1" link2="J2_1" reason="Adjacent"/>
<disable_collisions link1="J1_1" link2="J3_1" reason="Never"/>
<disable_collisions link1="J1_1" link2="J4_1" reason="Never"/>
<disable_collisions link1="J1_1" link2="base_link" reason="Adjacent"/>
<disable_collisions link1="J2_1" link2="J3_1" reason="Adjacent"/>
<disable_collisions link1="J3_1" link2="J4_1" reason="Adjacent"/>
<disable_collisions link1="J3_1" link2="J5_1" reason="Never"/>
<disable_collisions link1="J3_1" link2="J6_1" reason="Never"/>
<disable_collisions link1="J3_1" link2="base_link" reason="Never"/>
<disable_collisions link1="J4_1" link2="J5_1" reason="Adjacent"/>
<disable_collisions link1="J4_1" link2="J6_1" reason="Never"/>
<disable_collisions link1="J5_1" link2="J6_1" reason="Adjacent"/>
</robot>

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" ?>
<robot name="dummy2" xmlns:xacro="http://www.ros.org/wiki/xacro" >
<transmission name="Joint1_tran">
<type>transmission_interface/SimpleTransmission</type>
<joint name="Joint1">
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
</joint>
<actuator name="Joint1_actr">
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
<mechanicalReduction>1</mechanicalReduction>
</actuator>
</transmission>
<transmission name="Joint2_tran">
<type>transmission_interface/SimpleTransmission</type>
<joint name="Joint2">
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
</joint>
<actuator name="Joint2_actr">
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
<mechanicalReduction>1</mechanicalReduction>
</actuator>
</transmission>
<transmission name="Joint3_tran">
<type>transmission_interface/SimpleTransmission</type>
<joint name="Joint3">
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
</joint>
<actuator name="Joint3_actr">
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
<mechanicalReduction>1</mechanicalReduction>
</actuator>
</transmission>
<transmission name="Joint4_tran">
<type>transmission_interface/SimpleTransmission</type>
<joint name="Joint4">
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
</joint>
<actuator name="Joint4_actr">
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
<mechanicalReduction>1</mechanicalReduction>
</actuator>
</transmission>
<transmission name="Joint5_tran">
<type>transmission_interface/SimpleTransmission</type>
<joint name="Joint5">
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
</joint>
<actuator name="Joint5_actr">
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
<mechanicalReduction>1</mechanicalReduction>
</actuator>
</transmission>
<transmission name="Joint6_tran">
<type>transmission_interface/SimpleTransmission</type>
<joint name="Joint6">
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
</joint>
<actuator name="Joint6_actr">
<hardwareInterface>hardware_interface/EffortJointInterface</hardwareInterface>
<mechanicalReduction>1</mechanicalReduction>
</actuator>
</transmission>
</robot>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name="dummy2">
<xacro:arg name="initial_positions_file" default="initial_positions.yaml" />
<!-- Import dummy2 urdf file -->
<xacro:include filename="$(find dummy2_description)/urdf/dummy2.xacro" />
<!-- Import control_xacro -->
<xacro:include filename="dummy2.ros2_control.xacro" />
<xacro:dummy2_ros2_control name="FakeSystem" initial_positions_file="$(arg initial_positions_file)"/>
</robot>

View File

@@ -0,0 +1,73 @@
###############################################
# Modify all parameters related to servoing here
###############################################
# adapt to dummy2 by Muzhxiaowen, check out the details on bilibili.com
use_gazebo: false # Whether the robot is started in a Gazebo simulation environment
## Properties of incoming commands
command_in_type: "unitless" # "unitless"> in the range [-1:1], as if from joystick. "speed_units"> cmds are in m/s and rad/s
scale:
# Scale parameters are only used if command_in_type=="unitless"
linear: 0.4 # Max linear velocity. Unit is [m/s]. Only used for Cartesian commands.
rotational: 0.8 # Max angular velocity. Unit is [rad/s]. Only used for Cartesian commands.
# Max joint angular/linear velocity. Only used for joint commands on joint_command_in_topic.
joint: 0.5
# Optionally override Servo's internal velocity scaling when near singularity or collision (0.0 = use internal velocity scaling)
# override_velocity_scaling_factor = 0.0 # valid range [0.0:1.0]
## Properties of outgoing commands
publish_period: 0.034 # 1/Nominal publish rate [seconds]
low_latency_mode: false # Set this to true to publish as soon as an incoming Twist command is received (publish_period is ignored)
# What type of topic does your robot driver expect?
# Currently supported are std_msgs/Float64MultiArray or trajectory_msgs/JointTrajectory
command_out_type: trajectory_msgs/JointTrajectory
# What to publish? Can save some bandwidth as most robots only require positions or velocities
publish_joint_positions: true
publish_joint_velocities: true
publish_joint_accelerations: false
## Plugins for smoothing outgoing commands
smoothing_filter_plugin_name: "online_signal_smoothing::ButterworthFilterPlugin"
# If is_primary_planning_scene_monitor is set to true, the Servo server's PlanningScene advertises the /get_planning_scene service,
# which other nodes can use as a source for information about the planning environment.
# NOTE: If a different node in your system is responsible for the "primary" planning scene instance (e.g. the MoveGroup node),
# then is_primary_planning_scene_monitor needs to be set to false.
is_primary_planning_scene_monitor: true
## MoveIt properties
move_group_name: dummy2_arm # Often 'manipulator' or 'arm'
planning_frame: base_link # The MoveIt planning frame. Often 'base_link' or 'world'
## Other frames
ee_frame_name: J6_1 # The name of the end effector link, used to return the EE pose
robot_link_command_frame: base_link # commands must be given in the frame of a robot link. Usually either the base or end effector
## Stopping behaviour
incoming_command_timeout: 0.1 # Stop servoing if X seconds elapse without a new command
# If 0, republish commands forever even if the robot is stationary. Otherwise, specify num. to publish.
# Important because ROS may drop some messages and we need the robot to halt reliably.
num_outgoing_halt_msgs_to_publish: 4
## Configure handling of singularities and joint limits
lower_singularity_threshold: 170.0 # Start decelerating when the condition number hits this (close to singularity)
hard_stop_singularity_threshold: 3000.0 # Stop when the condition number hits this
joint_limit_margin: 0.1 # added as a buffer to joint limits [radians]. If moving quickly, make this larger.
leaving_singularity_threshold_multiplier: 2.0 # Multiply the hard stop limit by this when leaving singularity (see https://github.com/ros-planning/moveit2/pull/620)
## Topic names
cartesian_command_in_topic: ~/delta_twist_cmds # Topic for incoming Cartesian twist commands
joint_command_in_topic: ~/delta_joint_cmds # Topic for incoming joint angle commands
joint_topic: /joint_states
status_topic: ~/status # Publish status to this topic
command_out_topic: /dummy2_arm_controller/joint_trajectory # Publish outgoing commands here
## Collision checking for the entire robot body
check_collisions: true # Check collisions?
collision_check_rate: 10.0 # [Hz] Collision-checking can easily bog down a CPU if done too often.
self_collision_proximity_threshold: 0.001 # Start decelerating when a self-collision is this far [m]
scene_collision_proximity_threshold: 0.002 # Start decelerating when a scene collision is this far [m]

View File

@@ -0,0 +1,9 @@
# Default initial positions for dummy2's ros2_control fake system
initial_positions:
Joint1: 0
Joint2: 0
Joint3: 0
Joint4: 0
Joint5: 0
Joint6: 0

View File

@@ -0,0 +1,40 @@
# joint_limits.yaml allows the dynamics properties specified in the URDF to be overwritten or augmented as needed
# For beginners, we downscale velocity and acceleration limits.
# You can always specify higher scaling factors (<= 1.0) in your motion requests. # Increase the values below to 1.0 to always move at maximum speed.
default_velocity_scaling_factor: 0.1
default_acceleration_scaling_factor: 0.1
# Specific joint properties can be changed with the keys [max_position, min_position, max_velocity, max_acceleration]
# Joint limits can be turned off with [has_velocity_limits, has_acceleration_limits]
joint_limits:
joint_1:
has_velocity_limits: true
max_velocity: 2.0
has_acceleration_limits: false
max_acceleration: 0
joint_2:
has_velocity_limits: true
max_velocity: 2.0
has_acceleration_limits: false
max_acceleration: 0
joint_3:
has_velocity_limits: true
max_velocity: 2.0
has_acceleration_limits: false
max_acceleration: 0
joint_4:
has_velocity_limits: true
max_velocity: 2.0
has_acceleration_limits: false
max_acceleration: 0
joint_5:
has_velocity_limits: true
max_velocity: 2.0
has_acceleration_limits: false
max_acceleration: 0
joint_6:
has_velocity_limits: true
max_velocity: 2.0
has_acceleration_limits: false
max_acceleration: 0

View File

@@ -0,0 +1,4 @@
dummy2_arm:
kinematics_solver: kdl_kinematics_plugin/KDLKinematicsPlugin
kinematics_solver_search_resolution: 0.0050000000000000001
kinematics_solver_timeout: 0.5

View File

@@ -0,0 +1,60 @@
<?xml version="1.0"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro">
<xacro:macro name="dummy2_robot_ros2_control" params="device_name mesh_path">
<xacro:property name="initial_positions" value="${load_yaml(mesh_path + '/devices/dummy2_robot/config/initial_positions.yaml')['initial_positions']}"/>
<ros2_control name="${device_name}dummy2" type="system">
<hardware>
<!-- By default, set up controllers for simulation. This won't work on real hardware -->
<plugin>mock_components/GenericSystem</plugin>
</hardware>
<!-- <plugin>mock_components/GenericSystem</plugin> -->
<joint name="${device_name}Joint1">
<command_interface name="position"/>
<state_interface name="position">
<param name="initial_value">${initial_positions['Joint1']}</param>
</state_interface>
<state_interface name="velocity"/>
</joint>
<joint name="${device_name}Joint2">
<command_interface name="position"/>
<state_interface name="position">
<param name="initial_value">${initial_positions['Joint2']}</param>
</state_interface>
<state_interface name="velocity"/>
</joint>
<joint name="${device_name}Joint3">
<command_interface name="position"/>
<state_interface name="position">
<param name="initial_value">${initial_positions['Joint3']}</param>
</state_interface>
<state_interface name="velocity"/>
</joint>
<joint name="${device_name}Joint4">
<command_interface name="position"/>
<state_interface name="position">
<param name="initial_value">${initial_positions['Joint4']}</param>
</state_interface>
<state_interface name="velocity"/>
</joint>
<joint name="${device_name}Joint5">
<command_interface name="position"/>
<state_interface name="position">
<param name="initial_value">${initial_positions['Joint5']}</param>
</state_interface>
<state_interface name="velocity"/>
</joint>
<joint name="${device_name}Joint6">
<command_interface name="position"/>
<state_interface name="position">
<param name="initial_value">${initial_positions['Joint6']}</param>
</state_interface>
<state_interface name="velocity"/>
</joint>
</ros2_control>
</xacro:macro>
</robot>

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro">
<xacro:macro name="dummy2_robot_srdf" params="device_name">
<!--GROUPS: Representation of a set of joints and links. This can be useful for specifying DOF to plan for, defining arms, end effectors, etc-->
<!--LINKS: When a link is specified, the parent joint of that link (if it exists) is automatically included-->
<!--JOINTS: When a joint is specified, the child link of that joint (which will always exist) is automatically included-->
<!--CHAINS: When a chain is specified, all the links along the chain (including endpoints) are included in the group. Additionally, all the joints that are parents to included links are also included. This means that joints along the chain and the parent joint of the base link are included in the group-->
<!--SUBGROUPS: Groups can also be formed by referencing to already defined group names
This is a format for representing semantic information about the robot structure.
A URDF file must exist for this robot as well, where the joints and the links that are referenced are defined
-->
<group name="${device_name}dummy2_arm">
<joint name="${device_name}virtual_joint"/>
<joint name="${device_name}Joint1"/>
<joint name="${device_name}Joint2"/>
<joint name="${device_name}Joint3"/>
<joint name="${device_name}Joint4"/>
<joint name="${device_name}Joint5"/>
<joint name="${device_name}Joint6"/>
</group>
<!--GROUP STATES: Purpose: Define a named state for a particular group, in terms of joint values. This is useful to define states like 'folded arms'-->
<group_state name="home" group="${device_name}dummy2_arm">
<joint name="${device_name}Joint1" value="0"/>
<joint name="${device_name}Joint2" value="0"/>
<joint name="${device_name}Joint3" value="0"/>
<joint name="${device_name}Joint4" value="0"/>
<joint name="${device_name}Joint5" value="0"/>
<joint name="${device_name}Joint6" value="0"/>
</group_state>
<!--VIRTUAL JOINT: Purpose: this element defines a virtual joint between a robot link and an external frame of reference (considered fixed with respect to the robot)-->
<virtual_joint name="${device_name}virtual_joint" type="fixed" parent_frame="world" child_link="${device_name}base_link"/>
<!--DISABLE COLLISIONS: By default it is assumed that any link of the robot could potentially come into collision with any other link in the robot. This tag disables collision checking between a specified pair of links. -->
<disable_collisions link1="${device_name}J1_1" link2="${device_name}J2_1" reason="Adjacent"/>
<disable_collisions link1="${device_name}J1_1" link2="${device_name}J3_1" reason="Never"/>
<disable_collisions link1="${device_name}J1_1" link2="${device_name}J4_1" reason="Never"/>
<disable_collisions link1="${device_name}J1_1" link2="${device_name}base_link" reason="Adjacent"/>
<disable_collisions link1="${device_name}J2_1" link2="${device_name}J3_1" reason="Adjacent"/>
<disable_collisions link1="${device_name}J3_1" link2="${device_name}J4_1" reason="Adjacent"/>
<disable_collisions link1="${device_name}J3_1" link2="${device_name}J5_1" reason="Never"/>
<disable_collisions link1="${device_name}J3_1" link2="${device_name}J6_1" reason="Never"/>
<disable_collisions link1="${device_name}J3_1" link2="${device_name}base_link" reason="Never"/>
<disable_collisions link1="${device_name}J4_1" link2="${device_name}J5_1" reason="Adjacent"/>
<disable_collisions link1="${device_name}J4_1" link2="${device_name}J6_1" reason="Never"/>
<disable_collisions link1="${device_name}J5_1" link2="${device_name}J6_1" reason="Adjacent"/>
</xacro:macro>
</robot>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" ?>
<robot name="dummy2" xmlns:xacro="http://www.ros.org/wiki/xacro" >
<material name="silver">
<color rgba="0.700 0.700 0.700 1.000"/>
</material>
</robot>

View File

@@ -0,0 +1,14 @@
{
"arm": {
"joint_names": [
"joint_1",
"joint_2",
"joint_3",
"joint_4",
"joint_5",
"joint_6"
],
"base_link_name": "base_link",
"end_effector_name": "J6_1"
}
}

View File

@@ -0,0 +1,51 @@
Panels:
- Class: rviz_common/Displays
Name: Displays
Property Tree Widget:
Expanded:
- /MotionPlanning1
- Class: rviz_common/Help
Name: Help
- Class: rviz_common/Views
Name: Views
Visualization Manager:
Displays:
- Class: rviz_default_plugins/Grid
Name: Grid
Value: true
- Class: moveit_rviz_plugin/MotionPlanning
Name: MotionPlanning
Planned Path:
Loop Animation: true
State Display Time: 0.05 s
Trajectory Topic: display_planned_path
Planning Scene Topic: monitored_planning_scene
Robot Description: robot_description
Scene Geometry:
Scene Alpha: 1
Scene Robot:
Robot Alpha: 0.5
Value: true
Global Options:
Fixed Frame: base_link
Tools:
- Class: rviz_default_plugins/Interact
- Class: rviz_default_plugins/MoveCamera
- Class: rviz_default_plugins/Select
Value: true
Views:
Current:
Class: rviz_default_plugins/Orbit
Distance: 2.0
Focal Point:
X: -0.1
Y: 0.25
Z: 0.30
Name: Current View
Pitch: 0.5
Target Frame: base_link
Yaw: -0.623
Window Geometry:
Height: 975
QMainWindow State: 000000ff00000000fd0000000100000000000002b400000375fc0200000005fb00000044004d006f00740069006f006e0050006c0061006e006e0069006e00670020002d0020005400720061006a006500630074006f0072007900200053006c00690064006500720000000000ffffffff0000004100fffffffb000000100044006900730070006c006100790073010000003d00000123000000c900fffffffb0000001c004d006f00740069006f006e0050006c0061006e006e0069006e00670100000166000001910000018800fffffffb0000000800480065006c0070000000029a0000006e0000006e00fffffffb0000000a0056006900650077007301000002fd000000b5000000a400ffffff000001f60000037500000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000
Width: 1200

View File

@@ -0,0 +1,21 @@
# MoveIt uses this configuration for controller management
moveit_controller_manager: moveit_simple_controller_manager/MoveItSimpleControllerManager
moveit_simple_controller_manager:
controller_names:
- dummy2_arm_controller
dummy2_arm_controller:
type: FollowJointTrajectory
action_ns: follow_joint_trajectory
default: true
joints:
- Joint1
- Joint2
- Joint3
- Joint4
- Joint5
- Joint6
action_ns: follow_joint_trajectory
default: true

View File

@@ -0,0 +1,39 @@
dummy2_robot:
# Physical properties for each link
link_masses:
base_link: 5.0
link_1: 3.0
link_2: 2.5
link_3: 2.0
link_4: 1.5
link_5: 1.0
link_6: 0.5
# Center of mass for each link (relative to joint frame)
link_com:
base_link: [0.0, 0.0, 0.05]
link_1: [0.0, 0.0, 0.05]
link_2: [0.1, 0.0, 0.0]
link_3: [0.08, 0.0, 0.0]
link_4: [0.0, 0.0, 0.05]
link_5: [0.0, 0.0, 0.03]
link_6: [0.0, 0.0, 0.02]
# Moment of inertia matrices
link_inertias:
base_link: [0.02, 0.0, 0.0, 0.02, 0.0, 0.02]
link_1: [0.01, 0.0, 0.0, 0.01, 0.0, 0.01]
link_2: [0.008, 0.0, 0.0, 0.008, 0.0, 0.008]
link_3: [0.006, 0.0, 0.0, 0.006, 0.0, 0.006]
link_4: [0.004, 0.0, 0.0, 0.004, 0.0, 0.004]
link_5: [0.002, 0.0, 0.0, 0.002, 0.0, 0.002]
link_6: [0.001, 0.0, 0.0, 0.001, 0.0, 0.001]
# Motor specifications
motor_specs:
joint_1: { max_torque: 150.0, max_speed: 2.0, gear_ratio: 100 }
joint_2: { max_torque: 150.0, max_speed: 2.0, gear_ratio: 100 }
joint_3: { max_torque: 150.0, max_speed: 2.0, gear_ratio: 100 }
joint_4: { max_torque: 50.0, max_speed: 2.0, gear_ratio: 50 }
joint_5: { max_torque: 50.0, max_speed: 2.0, gear_ratio: 50 }
joint_6: { max_torque: 25.0, max_speed: 2.0, gear_ratio: 25 }

View File

@@ -0,0 +1,6 @@
# Limits for the Pilz planner
cartesian_limits:
max_trans_vel: 1.0
max_trans_acc: 2.25
max_trans_dec: -5.0
max_rot_vel: 1.57

View File

@@ -0,0 +1,26 @@
# This config file is used by ros2_control
controller_manager:
ros__parameters:
update_rate: 100 # Hz
dummy2_arm_controller:
type: joint_trajectory_controller/JointTrajectoryController
joint_state_broadcaster:
type: joint_state_broadcaster/JointStateBroadcaster
dummy2_arm_controller:
ros__parameters:
joints:
- Joint1
- Joint2
- Joint3
- Joint4
- Joint5
- Joint6
command_interfaces:
- position
state_interfaces:
- position
- velocity

View File

@@ -0,0 +1,35 @@
dummy2_robot:
# Visual appearance settings
materials:
base_material:
color: [0.8, 0.8, 0.8, 1.0] # Light gray
metallic: 0.1
roughness: 0.3
link_material:
color: [0.2, 0.2, 0.8, 1.0] # Blue
metallic: 0.3
roughness: 0.2
joint_material:
color: [0.6, 0.6, 0.6, 1.0] # Dark gray
metallic: 0.5
roughness: 0.1
camera_material:
color: [0.1, 0.1, 0.1, 1.0] # Black
metallic: 0.0
roughness: 0.8
# Mesh scaling factors
mesh_scale: [0.001, 0.001, 0.001] # Convert mm to m
# Collision geometry simplification
collision_geometries:
base_link: "cylinder" # radius: 0.08, height: 0.1
link_1: "cylinder" # radius: 0.05, height: 0.15
link_2: "box" # size: [0.2, 0.08, 0.08]
link_3: "box" # size: [0.15, 0.06, 0.06]
link_4: "cylinder" # radius: 0.03, height: 0.1
link_5: "cylinder" # radius: 0.025, height: 0.06
link_6: "cylinder" # radius: 0.02, height: 0.04

View File

@@ -0,0 +1,237 @@
<?xml version="1.0" ?>
<robot name="dummy2" xmlns:xacro="http://www.ros.org/wiki/xacro">
<xacro:include filename="$(find dummy2_description)/urdf/materials.xacro" />
<xacro:include filename="$(find dummy2_description)/urdf/dummy2.trans" />
<xacro:include filename="$(find dummy2_description)/urdf/dummy2.gazebo" />
<link name="world" />
<joint name="world_joint" type="fixed">
<parent link="world" />
<child link = "base_link" />
<origin xyz="0.0 0.0 0.0" rpy="0.0 0.0 0.0" />
</joint>
<link name="base_link">
<inertial>
<origin xyz="0.00010022425916431473 -6.186605493937309e-05 0.05493640543484716" rpy="0 0 0"/>
<mass value="1.2152141810431654"/>
<inertia ixx="0.002105" iyy="0.002245" izz="0.002436" ixy="-0.0" iyz="-1.1e-05" ixz="0.0"/>
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/base_link.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/base_link.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="J1_1">
<inertial>
<origin xyz="-0.00617659688932347 0.007029599744830012 0.012866826083045027" rpy="0 0 0"/>
<mass value="0.1332774369186824"/>
<inertia ixx="6e-05" iyy="5e-05" izz="8.8e-05" ixy="2.1e-05" iyz="-1.4e-05" ixz="8e-06"/>
</inertial>
<visual>
<origin xyz="-0.0001 0.000289 -0.097579" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J1_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.0001 0.000289 -0.097579" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J1_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="J2_1">
<inertial>
<origin xyz="0.019335709221765855 0.0019392793940843159 0.07795928103332703" rpy="0 0 0"/>
<mass value="1.9268013917303417"/>
<inertia ixx="0.006165" iyy="0.006538" izz="0.00118" ixy="-3e-06" iyz="4.7e-05" ixz="0.0007"/>
</inertial>
<visual>
<origin xyz="0.011539 -0.034188 -0.12478" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J2_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="0.011539 -0.034188 -0.12478" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J2_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="J3_1">
<inertial>
<origin xyz="-0.010672101243726572 -0.02723871972304964 0.04876701375652198" rpy="0 0 0"/>
<mass value="0.30531962155452225"/>
<inertia ixx="0.00029" iyy="0.000238" izz="0.000191" ixy="-1.3e-05" iyz="4.1e-05" ixz="3e-05"/>
</inertial>
<visual>
<origin xyz="-0.023811 -0.034188 -0.28278" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J3_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.023811 -0.034188 -0.28278" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J3_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="J4_1">
<inertial>
<origin xyz="-0.005237398377441591 0.06002028183461833 0.0005891767740203724" rpy="0 0 0"/>
<mass value="0.14051172121899885"/>
<inertia ixx="0.000245" iyy="7.9e-05" izz="0.00027" ixy="1.6e-05" iyz="-2e-06" ixz="1e-06"/>
</inertial>
<visual>
<origin xyz="-0.010649 -0.038288 -0.345246" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J4_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.010649 -0.038288 -0.345246" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J4_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="J5_1">
<inertial>
<origin xyz="-0.014389813882964664 0.07305218143664277 -0.0009243405950149497" rpy="0 0 0"/>
<mass value="0.7783315754227634"/>
<inertia ixx="0.000879" iyy="0.000339" izz="0.000964" ixy="0.000146" iyz="1e-06" ixz="-5e-06"/>
</inertial>
<visual>
<origin xyz="-0.031949 -0.148289 -0.345246" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J5_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.031949 -0.148289 -0.345246" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J5_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="J6_1">
<inertial>
<origin xyz="3.967160300787087e-07 0.0004995066702210837 1.4402781733924286e-07" rpy="0 0 0"/>
<mass value="0.0020561527568204153"/>
<inertia ixx="0.0" iyy="0.0" izz="0.0" ixy="0.0" iyz="0.0" ixz="0.0"/>
</inertial>
<visual>
<origin xyz="-0.012127 -0.267789 -0.344021" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J6_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.012127 -0.267789 -0.344021" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J6_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="camera">
<inertial>
<origin xyz="-0.0006059984983273845 0.0005864706438700462 0.04601775357664567" rpy="0 0 0"/>
<mass value="0.21961029019655884"/>
<inertia ixx="2.9e-05" iyy="0.000206" izz="0.000198" ixy="-0.0" iyz="2e-06" ixz="-0.0"/>
</inertial>
<visual>
<origin xyz="-0.012661 -0.239774 -0.37985" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/camera_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.012661 -0.239774 -0.37985" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/camera_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<joint name="Joint1" type="revolute">
<origin xyz="0.0001 -0.000289 0.097579" rpy="0 0 0"/>
<parent link="base_link"/>
<child link="J1_1"/>
<axis xyz="-0.0 -0.0 1.0"/>
<limit upper="3.054326" lower="-3.054326" effort="100" velocity="100"/>
</joint>
<joint name="Joint2" type="revolute">
<origin xyz="-0.011639 0.034477 0.027201" rpy="0 0 0"/>
<parent link="J1_1"/>
<child link="J2_1"/>
<axis xyz="1.0 0.0 -0.0"/>
<limit upper="1.308997" lower="-2.007129" effort="100" velocity="100"/>
</joint>
<joint name="Joint3" type="revolute">
<origin xyz="0.03535 0.0 0.158" rpy="0 0 0"/>
<parent link="J2_1"/>
<child link="J3_1"/>
<axis xyz="-1.0 0.0 -0.0"/>
<limit upper="1.570796" lower="-1.047198" effort="100" velocity="100"/>
</joint>
<joint name="Joint4" type="revolute">
<origin xyz="-0.013162 0.0041 0.062466" rpy="0 0 0"/>
<parent link="J3_1"/>
<child link="J4_1"/>
<axis xyz="0.0 1.0 -0.0"/>
<limit upper="3.141593" lower="-3.141593" effort="100" velocity="100"/>
</joint>
<joint name="Joint5" type="revolute">
<origin xyz="0.0213 0.110001 0.0" rpy="0 0 0"/>
<parent link="J4_1"/>
<child link="J5_1"/>
<axis xyz="-1.0 -0.0 -0.0"/>
<limit upper="2.094395" lower="-1.919862" effort="100" velocity="100"/>
</joint>
<joint name="Joint6" type="continuous">
<origin xyz="-0.019822 0.1195 -0.001225" rpy="0 0 0"/>
<parent link="J5_1"/>
<child link="J6_1"/>
<axis xyz="0.0 -1.0 0.0"/>
</joint>
<joint name="camera" type="fixed">
<origin xyz="-0.019988 0.091197 0.024883" rpy="0 0 0"/>
<parent link="J5_1"/>
<child link="camera"/>
<axis xyz="1.0 -0.0 0.0"/>
<limit upper="0.0" lower="0.0" effort="100" velocity="100"/>
</joint>
</robot>

View File

@@ -0,0 +1,37 @@
joint_limits:
joint_1:
effort: 150
velocity: 2.0
lower: !degrees -180
upper: !degrees 180
joint_2:
effort: 150
velocity: 2.0
lower: !degrees -90
upper: !degrees 90
joint_3:
effort: 150
velocity: 2.0
lower: !degrees -90
upper: !degrees 90
joint_4:
effort: 50
velocity: 2.0
lower: !degrees -180
upper: !degrees 180
joint_5:
effort: 50
velocity: 2.0
lower: !degrees -90
upper: !degrees 90
joint_6:
effort: 25
velocity: 2.0
lower: !degrees -180
upper: !degrees 180

View File

@@ -0,0 +1,249 @@
<?xml version="1.0" ?>
<robot name="dummy2" xmlns:xacro="http://www.ros.org/wiki/xacro">
<xacro:macro name="dummy2_robot" params="mesh_path:='' parent_link:='' station_name:='' device_name:='' x:=0 y:=0 z:=0 rx:=0 ry:=0 r:=0">
<xacro:include filename="${mesh_path}/devices/dummy2_robot/config/materials.xacro" />
<xacro:include filename="${mesh_path}/devices/dummy2_robot/config/dummy2.trans" />
<joint name="${station_name}${device_name}base_link_joint" type="fixed">
<origin xyz="${x} ${y} ${z}" rpy="${rx} ${ry} ${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 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 xyz="0.00010022425916431473 -6.186605493937309e-05 0.05493640543484716" rpy="0 0 0"/>
<mass value="1.2152141810431654"/>
<inertia ixx="0.002105" iyy="0.002245" izz="0.002436" ixy="-0.0" iyz="-1.1e-05" ixz="0.0"/>
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/base_link.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/base_link.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="${station_name}${device_name}J1_1">
<inertial>
<origin xyz="-0.00617659688932347 0.007029599744830012 0.012866826083045027" rpy="0 0 0"/>
<mass value="0.1332774369186824"/>
<inertia ixx="6e-05" iyy="5e-05" izz="8.8e-05" ixy="2.1e-05" iyz="-1.4e-05" ixz="8e-06"/>
</inertial>
<visual>
<origin xyz="-0.0001 0.000289 -0.097579" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/J1_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.0001 0.000289 -0.097579" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/J1_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="${station_name}${device_name}J2_1">
<inertial>
<origin xyz="0.019335709221765855 0.0019392793940843159 0.07795928103332703" rpy="0 0 0"/>
<mass value="1.9268013917303417"/>
<inertia ixx="0.006165" iyy="0.006538" izz="0.00118" ixy="-3e-06" iyz="4.7e-05" ixz="0.0007"/>
</inertial>
<visual>
<origin xyz="0.011539 -0.034188 -0.12478" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/J2_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="0.011539 -0.034188 -0.12478" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/J2_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="${station_name}${device_name}J3_1">
<inertial>
<origin xyz="-0.010672101243726572 -0.02723871972304964 0.04876701375652198" rpy="0 0 0"/>
<mass value="0.30531962155452225"/>
<inertia ixx="0.00029" iyy="0.000238" izz="0.000191" ixy="-1.3e-05" iyz="4.1e-05" ixz="3e-05"/>
</inertial>
<visual>
<origin xyz="-0.023811 -0.034188 -0.28278" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/J3_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.023811 -0.034188 -0.28278" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/J3_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="${station_name}${device_name}J4_1">
<inertial>
<origin xyz="-0.005237398377441591 0.06002028183461833 0.0005891767740203724" rpy="0 0 0"/>
<mass value="0.14051172121899885"/>
<inertia ixx="0.000245" iyy="7.9e-05" izz="0.00027" ixy="1.6e-05" iyz="-2e-06" ixz="1e-06"/>
</inertial>
<visual>
<origin xyz="-0.010649 -0.038288 -0.345246" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/J4_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.010649 -0.038288 -0.345246" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/J4_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="${station_name}${device_name}J5_1">
<inertial>
<origin xyz="-0.014389813882964664 0.07305218143664277 -0.0009243405950149497" rpy="0 0 0"/>
<mass value="0.7783315754227634"/>
<inertia ixx="0.000879" iyy="0.000339" izz="0.000964" ixy="0.000146" iyz="1e-06" ixz="-5e-06"/>
</inertial>
<visual>
<origin xyz="-0.031949 -0.148289 -0.345246" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/J5_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.031949 -0.148289 -0.345246" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/J5_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="${station_name}${device_name}J6_1">
<inertial>
<origin xyz="3.967160300787087e-07 0.0004995066702210837 1.4402781733924286e-07" rpy="0 0 0"/>
<mass value="0.0020561527568204153"/>
<inertia ixx="0.0" iyy="0.0" izz="0.0" ixy="0.0" iyz="0.0" ixz="0.0"/>
</inertial>
<visual>
<origin xyz="-0.012127 -0.267789 -0.344021" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/J6_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.012127 -0.267789 -0.344021" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/J6_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="${station_name}${device_name}camera">
<inertial>
<origin xyz="-0.0006059984983273845 0.0005864706438700462 0.04601775357664567" rpy="0 0 0"/>
<mass value="0.21961029019655884"/>
<inertia ixx="2.9e-05" iyy="0.000206" izz="0.000198" ixy="-0.0" iyz="2e-06" ixz="-0.0"/>
</inertial>
<visual>
<origin xyz="-0.012661 -0.239774 -0.37985" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/camera_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.012661 -0.239774 -0.37985" rpy="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/dummy2_robot/meshes/camera_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<joint name="${station_name}${device_name}Joint1" type="revolute">
<origin xyz="0.0001 -0.000289 0.097579" rpy="0 0 0"/>
<parent link="${station_name}${device_name}base_link"/>
<child link="${station_name}${device_name}J1_1"/>
<axis xyz="-0.0 -0.0 1.0"/>
<limit upper="3.054326" lower="-3.054326" effort="100" velocity="100"/>
</joint>
<joint name="${station_name}${device_name}Joint2" type="revolute">
<origin xyz="-0.011639 0.034477 0.027201" rpy="0 0 0"/>
<parent link="${station_name}${device_name}J1_1"/>
<child link="${station_name}${device_name}J2_1"/>
<axis xyz="1.0 0.0 -0.0"/>
<limit upper="1.308997" lower="-2.007129" effort="100" velocity="100"/>
</joint>
<joint name="${station_name}${device_name}Joint3" type="revolute">
<origin xyz="0.03535 0.0 0.158" rpy="0 0 0"/>
<parent link="${station_name}${device_name}J2_1"/>
<child link="${station_name}${device_name}J3_1"/>
<axis xyz="-1.0 0.0 -0.0"/>
<limit upper="1.570796" lower="-1.047198" effort="100" velocity="100"/>
</joint>
<joint name="${station_name}${device_name}Joint4" type="revolute">
<origin xyz="-0.013162 0.0041 0.062466" rpy="0 0 0"/>
<parent link="${station_name}${device_name}J3_1"/>
<child link="${station_name}${device_name}J4_1"/>
<axis xyz="0.0 1.0 -0.0"/>
<limit upper="3.141593" lower="-3.141593" effort="100" velocity="100"/>
</joint>
<joint name="${station_name}${device_name}Joint5" type="revolute">
<origin xyz="0.0213 0.110001 0.0" rpy="0 0 0"/>
<parent link="${station_name}${device_name}J4_1"/>
<child link="${station_name}${device_name}J5_1"/>
<axis xyz="-1.0 -0.0 -0.0"/>
<limit upper="2.094395" lower="-1.919862" effort="100" velocity="100"/>
</joint>
<joint name="${station_name}${device_name}Joint6" type="continuous">
<origin xyz="-0.019822 0.1195 -0.001225" rpy="0 0 0"/>
<parent link="${station_name}${device_name}J5_1"/>
<child link="${station_name}${device_name}J6_1"/>
<axis xyz="0.0 -1.0 0.0"/>
</joint>
<joint name="${station_name}${device_name}camera" type="fixed">
<origin xyz="-0.019988 0.091197 0.024883" rpy="0 0 0"/>
<parent link="${station_name}${device_name}J5_1"/>
<child link="${station_name}${device_name}camera"/>
<axis xyz="1.0 -0.0 0.0"/>
<limit upper="0.0" lower="0.0" effort="100" velocity="100"/>
</joint>
</xacro:macro>
</robot>

View File

@@ -0,0 +1,237 @@
<?xml version="1.0" ?>
<robot name="dummy2" xmlns:xacro="http://www.ros.org/wiki/xacro">
<xacro:include filename="$(find dummy2_description)/urdf/materials.xacro" />
<xacro:include filename="$(find dummy2_description)/urdf/dummy2.trans" />
<xacro:include filename="$(find dummy2_description)/urdf/dummy2.gazebo" />
<link name="world" />
<joint name="world_joint" type="fixed">
<parent link="world" />
<child link = "base_link" />
<origin xyz="0.0 0.0 0.0" rpy="0.0 0.0 0.0" />
</joint>
<link name="base_link">
<inertial>
<origin xyz="0.00010022425916431473 -6.186605493937309e-05 0.05493640543484716" rpy="0 0 0"/>
<mass value="1.2152141810431654"/>
<inertia ixx="0.002105" iyy="0.002245" izz="0.002436" ixy="-0.0" iyz="-1.1e-05" ixz="0.0"/>
</inertial>
<visual>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/base_link.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="0 0 0" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/base_link.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="J1_1">
<inertial>
<origin xyz="-0.00617659688932347 0.007029599744830012 0.012866826083045027" rpy="0 0 0"/>
<mass value="0.1332774369186824"/>
<inertia ixx="6e-05" iyy="5e-05" izz="8.8e-05" ixy="2.1e-05" iyz="-1.4e-05" ixz="8e-06"/>
</inertial>
<visual>
<origin xyz="-0.0001 0.000289 -0.097579" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J1_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.0001 0.000289 -0.097579" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J1_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="J2_1">
<inertial>
<origin xyz="0.019335709221765855 0.0019392793940843159 0.07795928103332703" rpy="0 0 0"/>
<mass value="1.9268013917303417"/>
<inertia ixx="0.006165" iyy="0.006538" izz="0.00118" ixy="-3e-06" iyz="4.7e-05" ixz="0.0007"/>
</inertial>
<visual>
<origin xyz="0.011539 -0.034188 -0.12478" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J2_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="0.011539 -0.034188 -0.12478" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J2_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="J3_1">
<inertial>
<origin xyz="-0.010672101243726572 -0.02723871972304964 0.04876701375652198" rpy="0 0 0"/>
<mass value="0.30531962155452225"/>
<inertia ixx="0.00029" iyy="0.000238" izz="0.000191" ixy="-1.3e-05" iyz="4.1e-05" ixz="3e-05"/>
</inertial>
<visual>
<origin xyz="-0.023811 -0.034188 -0.28278" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J3_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.023811 -0.034188 -0.28278" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J3_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="J4_1">
<inertial>
<origin xyz="-0.005237398377441591 0.06002028183461833 0.0005891767740203724" rpy="0 0 0"/>
<mass value="0.14051172121899885"/>
<inertia ixx="0.000245" iyy="7.9e-05" izz="0.00027" ixy="1.6e-05" iyz="-2e-06" ixz="1e-06"/>
</inertial>
<visual>
<origin xyz="-0.010649 -0.038288 -0.345246" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J4_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.010649 -0.038288 -0.345246" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J4_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="J5_1">
<inertial>
<origin xyz="-0.014389813882964664 0.07305218143664277 -0.0009243405950149497" rpy="0 0 0"/>
<mass value="0.7783315754227634"/>
<inertia ixx="0.000879" iyy="0.000339" izz="0.000964" ixy="0.000146" iyz="1e-06" ixz="-5e-06"/>
</inertial>
<visual>
<origin xyz="-0.031949 -0.148289 -0.345246" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J5_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.031949 -0.148289 -0.345246" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J5_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="J6_1">
<inertial>
<origin xyz="3.967160300787087e-07 0.0004995066702210837 1.4402781733924286e-07" rpy="0 0 0"/>
<mass value="0.0020561527568204153"/>
<inertia ixx="0.0" iyy="0.0" izz="0.0" ixy="0.0" iyz="0.0" ixz="0.0"/>
</inertial>
<visual>
<origin xyz="-0.012127 -0.267789 -0.344021" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J6_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.012127 -0.267789 -0.344021" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/J6_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<link name="camera">
<inertial>
<origin xyz="-0.0006059984983273845 0.0005864706438700462 0.04601775357664567" rpy="0 0 0"/>
<mass value="0.21961029019655884"/>
<inertia ixx="2.9e-05" iyy="0.000206" izz="0.000198" ixy="-0.0" iyz="2e-06" ixz="-0.0"/>
</inertial>
<visual>
<origin xyz="-0.012661 -0.239774 -0.37985" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/camera_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
<material name="silver"/>
</visual>
<collision>
<origin xyz="-0.012661 -0.239774 -0.37985" rpy="0 0 0"/>
<geometry>
<mesh filename="file://$(find dummy2_description)/meshes/camera_1.stl" scale="0.001 0.001 0.001"/>
</geometry>
</collision>
</link>
<joint name="Joint1" type="revolute">
<origin xyz="0.0001 -0.000289 0.097579" rpy="0 0 0"/>
<parent link="base_link"/>
<child link="J1_1"/>
<axis xyz="-0.0 -0.0 1.0"/>
<limit upper="3.054326" lower="-3.054326" effort="100" velocity="100"/>
</joint>
<joint name="Joint2" type="revolute">
<origin xyz="-0.011639 0.034477 0.027201" rpy="0 0 0"/>
<parent link="J1_1"/>
<child link="J2_1"/>
<axis xyz="1.0 0.0 -0.0"/>
<limit upper="1.308997" lower="-2.007129" effort="100" velocity="100"/>
</joint>
<joint name="Joint3" type="revolute">
<origin xyz="0.03535 0.0 0.158" rpy="0 0 0"/>
<parent link="J2_1"/>
<child link="J3_1"/>
<axis xyz="-1.0 0.0 -0.0"/>
<limit upper="1.570796" lower="-1.047198" effort="100" velocity="100"/>
</joint>
<joint name="Joint4" type="revolute">
<origin xyz="-0.013162 0.0041 0.062466" rpy="0 0 0"/>
<parent link="J3_1"/>
<child link="J4_1"/>
<axis xyz="0.0 1.0 -0.0"/>
<limit upper="3.141593" lower="-3.141593" effort="100" velocity="100"/>
</joint>
<joint name="Joint5" type="revolute">
<origin xyz="0.0213 0.110001 0.0" rpy="0 0 0"/>
<parent link="J4_1"/>
<child link="J5_1"/>
<axis xyz="-1.0 -0.0 -0.0"/>
<limit upper="2.094395" lower="-1.919862" effort="100" velocity="100"/>
</joint>
<joint name="Joint6" type="continuous">
<origin xyz="-0.019822 0.1195 -0.001225" rpy="0 0 0"/>
<parent link="J5_1"/>
<child link="J6_1"/>
<axis xyz="0.0 -1.0 0.0"/>
</joint>
<joint name="camera" type="fixed">
<origin xyz="-0.019988 0.091197 0.024883" rpy="0 0 0"/>
<parent link="J5_1"/>
<child link="camera"/>
<axis xyz="1.0 -0.0 0.0"/>
<limit upper="0.0" lower="0.0" effort="100" velocity="100"/>
</joint>
</robot>

View File

View File

View File

View File

View File

@@ -14,6 +14,7 @@ from launch_ros.parameter_descriptions import ParameterFile
from unilabos.registry.registry import lab_registry
from ament_index_python.packages import get_package_share_directory
def get_pattern_matches(folder, pattern):
"""Given all the files in the folder, find those that match the pattern.
@@ -51,7 +52,7 @@ class ResourceVisualization:
self.launch_description = LaunchDescription()
self.resource_dict = resource
self.resource_model = {}
self.resource_type = ['deck', 'plate', 'container']
self.resource_type = ['deck', 'plate', 'container', 'tip_rack']
self.mesh_path = Path(__file__).parent.absolute()
self.enable_rviz = enable_rviz
registry = lab_registry
@@ -128,9 +129,9 @@ class ResourceVisualization:
# if node["parent"] is not None:
# 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))
new_dev.set("x",str(float(node["position"]["position"]["x"])/1000))
new_dev.set("y",str(float(node["position"]["position"]["y"])/1000))
new_dev.set("z",str(float(node["position"]["position"]["z"])/1000))
if "rotation" in node["config"]:
new_dev.set("rx",str(float(node["config"]["rotation"]["x"])))
new_dev.set("ry",str(float(node["config"]["rotation"]["y"])))
@@ -140,7 +141,7 @@ class ResourceVisualization:
new_dev.set(key, str(value))
# 添加ros2_controller
if node['class'].startswith('moveit.'):
if node['class'].find('moveit.')!= -1:
new_include_controller = etree.SubElement(self.root, f"{{{xacro_uri}}}include")
new_include_controller.set("filename", f"{str(self.mesh_path)}/devices/{model_config['mesh']}/config/macro.ros2_control.xacro")
new_controller = etree.SubElement(self.root, f"{{{xacro_uri}}}{model_config['mesh']}_ros2_control")
@@ -203,7 +204,24 @@ class ResourceVisualization:
Returns:
LaunchDescription: launch描述对象
"""
moveit_configs_utils_path = Path(get_package_share_directory("moveit_configs_utils"))
# 检查ROS 2环境变量
if "AMENT_PREFIX_PATH" not in os.environ:
raise OSError(
"ROS 2环境未正确设置。需要设置 AMENT_PREFIX_PATH 环境变量。\n"
"请确保:\n"
"1. 已安装ROS 2 (推荐使用 ros-humble-desktop-full)\n"
"2. 已激活Conda环境: conda activate unilab\n"
"3. 或手动source ROS 2 setup文件: source /opt/ros/humble/setup.bash\n"
"4. 或者使用 --backend simple 参数跳过ROS依赖"
)
try:
moveit_configs_utils_path = Path(get_package_share_directory("moveit_configs_utils"))
except Exception as e:
raise OSError(
f"无法找到moveit_configs_utils包。请确保ROS 2和MoveIt 2已正确安装。\n"
f"原始错误: {e}"
)
default_folder = moveit_configs_utils_path / "default_configs"
planning_pattern = re.compile("^(.*)_planning.yaml$")
pipelines = []
@@ -264,7 +282,8 @@ class ResourceVisualization:
parameters=[
{"robot_description": robot_description},
ros2_controllers,
]
],
env=dict(os.environ)
)
)
for controller in self.moveit_controllers_yaml['moveit_simple_controller_manager']['controller_names']:
@@ -274,6 +293,7 @@ class ResourceVisualization:
executable="spawner",
arguments=[f"{controller}", "--controller-manager", f"controller_manager"],
output="screen",
env=dict(os.environ)
)
)
controllers.append(
@@ -282,6 +302,7 @@ class ResourceVisualization:
executable="spawner",
arguments=["joint_state_broadcaster", "--controller-manager", f"controller_manager"],
output="screen",
env=dict(os.environ)
)
)
for i in controllers:
@@ -300,7 +321,8 @@ class ResourceVisualization:
'use_sim_time': False
},
# kinematics_dict
]
],
env=dict(os.environ)
)
@@ -331,7 +353,8 @@ class ResourceVisualization:
package='moveit_ros_move_group',
executable='move_group',
output='screen',
parameters=moveit_params
parameters=moveit_params,
env=dict(os.environ)
)
@@ -354,7 +377,8 @@ class ResourceVisualization:
robot_description_planning,
planning_pipelines,
]
],
env=dict(os.environ)
)
self.launch_description.add_action(rviz_node)

View File

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

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

Binary file not shown.

View File

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

View File

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

View File

@@ -5,11 +5,13 @@ Panels:
Property Tree Widget:
Expanded:
- /TF1/Tree1
- /PlanningScene1
- /PlanningScene1/Scene Geometry1
- /MotionPlanning1/Scene Geometry1
- /MotionPlanning1/Scene Robot1
- /MotionPlanning1/Planning Request1
Splitter Ratio: 0.5016146302223206
Tree Height: 1112
Tree Height: 563
- Class: rviz_common/Selection
Name: Selection
- Class: rviz_common/Tool Properties
@@ -91,7 +93,7 @@ Visualization Manager:
Planning Scene Topic: /monitored_planning_scene
Robot Description: robot_description
Scene Geometry:
Scene Alpha: 0.8999999761581421
Scene Alpha: 1
Scene Color: 50; 230; 50
Scene Display Time: 0.009999999776482582
Show Scene Geometry: true
@@ -567,25 +569,25 @@ Visualization Manager:
Pitch: 0.4297958016395569
Target Frame: <Fixed Frame>
Value: Orbit (rviz)
Yaw: 0.3525616228580475
Yaw: 0.36756160855293274
Saved: ~
Window Geometry:
Displays:
collapsed: false
Height: 2032
Height: 1088
Hide Left Dock: false
Hide Right Dock: true
MotionPlanning:
collapsed: true
collapsed: false
MotionPlanning - Trajectory Slider:
collapsed: false
QMainWindow State: 000000ff00000000fd0000000400000000000003a30000079bfc020000000bfb0000001200530065006c0065006300740069006f006e00000001e10000009b000000b000fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c0061007900730100000027000004c60000018200fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261fb000000280020002d0020005400720061006a006500630074006f0072007900200053006c00690064006500720000000000ffffffff0000000000000000fb00000044004d006f00740069006f006e0050006c0061006e006e0069006e00670020002d0020005400720061006a006500630074006f0072007900200053006c00690064006500720000000000ffffffff0000007a00fffffffb0000001c004d006f00740069006f006e0050006c0061006e006e0069006e006701000004f9000002c9000002b800ffffff000000010000010f00000387fc0200000003fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a00560069006500770073000000003b000003870000013200fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e10000019700000003000004420000003efc0100000002fb0000000800540069006d00650100000000000004420000000000000000fb0000000800540069006d0065010000000000000450000000000000000000000bc50000079b00000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730000000000ffffffff0000000000000000
QMainWindow State: 000000ff00000000fd0000000400000000000003a30000040bfc020000000bfb0000001200530065006c0065006300740069006f006e00000001e10000009b0000005c00fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c006100790073010000001700000271000000ca00fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261fb000000280020002d0020005400720061006a006500630074006f0072007900200053006c00690064006500720000000000ffffffff0000000000000000fb00000044004d006f00740069006f006e0050006c0061006e006e0069006e00670020002d0020005400720061006a006500630074006f0072007900200053006c00690064006500720000000000ffffffff0000004200fffffffb0000001c004d006f00740069006f006e0050006c0061006e006e0069006e0067010000028e000001940000018900ffffff000000010000010f00000387fc0200000003fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a00560069006500770073000000003b00000387000000a600fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e10000019700000003000004420000003efc0100000002fb0000000800540069006d00650100000000000004420000000000000000fb0000000800540069006d00650100000000000004500000000000000000000004110000040b00000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730000000000ffffffff0000000000000000
Selection:
collapsed: false
Tool Properties:
collapsed: false
Views:
collapsed: true
Width: 3956
X: 140
Y: 54
Width: 1978
X: 70
Y: 27

Some files were not shown because too many files have changed in this diff Show More