mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2026-02-09 00:15:10 +00:00
Compare commits
113 Commits
52dee44835
...
workstatio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5805f94e9a | ||
|
|
3adcc41ce8 | ||
|
|
243922caf4 | ||
|
|
079ec9d1b4 | ||
|
|
54cfaf15f3 | ||
|
|
1c9d2ee98a | ||
|
|
3fe8f4ca44 | ||
|
|
2476821dcc | ||
|
|
7b426ed5ae | ||
|
|
9bbae96447 | ||
|
|
10aabb7592 | ||
|
|
a5397ffe12 | ||
|
|
196e0f7e2b | ||
|
|
a632fd495e | ||
|
|
a8cc02a126 | ||
|
|
ad2e1432c6 | ||
|
|
c3b9583eac | ||
|
|
5c47cd0c8a | ||
|
|
63ab1af45d | ||
|
|
a8419dc0c3 | ||
|
|
34f05f2e25 | ||
|
|
0dc2488f02 | ||
|
|
f13156e792 | ||
|
|
13fd1ac572 | ||
|
|
f8ef6e0686 | ||
|
|
94a7b8aaca | ||
|
|
301bea639e | ||
|
|
4b5a83efa4 | ||
|
|
2889e9be2c | ||
|
|
304aebbba7 | ||
|
|
091c9fa247 | ||
|
|
67ca45a240 | ||
|
|
7aab2ea493 | ||
|
|
62f3a6d696 | ||
|
|
eb70ad0e18 | ||
|
|
768f43880e | ||
|
|
762c3c737c | ||
|
|
ace98a4472 | ||
|
|
41eaa88c6f | ||
|
|
a1a55a2c0a | ||
|
|
2eaa0ca729 | ||
|
|
6f8f070f40 | ||
|
|
da4bd927e0 | ||
|
|
01f8816597 | ||
|
|
e5006285df | ||
|
|
573c724a5c | ||
|
|
09549d2839 | ||
|
|
50c7777cea | ||
|
|
4888f02c09 | ||
|
|
779c9693d9 | ||
|
|
ffa841a41a | ||
|
|
fc669f09f8 | ||
|
|
2ca0311de6 | ||
|
|
94cdcbf24e | ||
|
|
1cd07915e7 | ||
|
|
b600fc666d | ||
|
|
9e214c56c1 | ||
|
|
bdf27a7e82 | ||
|
|
2493fb9f94 | ||
|
|
c7a0ff67a9 | ||
|
|
711a7c65fa | ||
|
|
cde7956896 | ||
|
|
95b6fd0451 | ||
|
|
513e848d89 | ||
|
|
58d1cc4720 | ||
|
|
5676dd6589 | ||
|
|
1ae274a833 | ||
|
|
22b88c8441 | ||
|
|
81bcc1907d | ||
|
|
8cffd3dc21 | ||
|
|
a722636938 | ||
|
|
f68340d932 | ||
|
|
361eae2f6d | ||
|
|
c25283ae04 | ||
|
|
961752fb0d | ||
|
|
55165024dd | ||
|
|
6ddceb8393 | ||
|
|
4e52c7d2f4 | ||
|
|
0b56efc89d | ||
|
|
a27b93396a | ||
|
|
2a60a6c27e | ||
|
|
5dda94044d | ||
|
|
0cfc6f45e3 | ||
|
|
831f4549f9 | ||
|
|
f4d4eb06d3 | ||
|
|
e3b8164f6b | ||
|
|
78c04acc2e | ||
|
|
cd0428ea78 | ||
|
|
bdddbd57ba | ||
|
|
a312de08a5 | ||
|
|
68513b5745 | ||
|
|
19027350fb | ||
|
|
bbbdb06bbc | ||
|
|
cd84e26126 | ||
|
|
ce5bab3af1 | ||
|
|
82d9ef6bf7 | ||
|
|
332b33c6f4 | ||
|
|
1ec642ee3a | ||
|
|
7d8e6d029b | ||
|
|
5ec8a57a1f | ||
|
|
ae3c1100ae | ||
|
|
14bc2e6cda | ||
|
|
9f823a4198 | ||
|
|
02c79363c1 | ||
|
|
227ff1284a | ||
|
|
4b7bde6be5 | ||
|
|
8a669ac35a | ||
|
|
a1538da39e | ||
|
|
0063df4cf3 | ||
|
|
e570ba4976 | ||
|
|
e8c1f76dbb | ||
|
|
f791c1a342 | ||
|
|
ea60cbe891 |
@@ -1,6 +1,6 @@
|
||||
package:
|
||||
name: unilabos
|
||||
version: 0.10.3
|
||||
version: 0.10.6
|
||||
|
||||
source:
|
||||
path: ../unilabos
|
||||
@@ -10,7 +10,6 @@ build:
|
||||
python:
|
||||
entry_points:
|
||||
- unilab = unilabos.app.main:main
|
||||
- unilab-register = unilabos.app.register:main
|
||||
script:
|
||||
- set PIP_NO_INDEX=
|
||||
- if: win
|
||||
@@ -61,7 +60,7 @@ requirements:
|
||||
- uvicorn
|
||||
- gradio
|
||||
- flask
|
||||
- websocket
|
||||
- websockets
|
||||
- ipython
|
||||
- jupyter
|
||||
- jupyros
|
||||
@@ -86,5 +85,5 @@ requirements:
|
||||
|
||||
about:
|
||||
repository: https://github.com/dptech-corp/Uni-Lab-OS
|
||||
license: GPL-3.0
|
||||
license: GPL-3.0-only
|
||||
description: "Uni-Lab-OS"
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -246,3 +246,5 @@ local_test2.py
|
||||
ros-humble-unilabos-msgs-0.9.13-h6403a04_5.tar.bz2
|
||||
*.bz2
|
||||
test_config.py
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
# Uni-Lab-OS
|
||||
|
||||
<!-- Language switcher -->
|
||||
|
||||
**English** | [中文](README_zh.md)
|
||||
|
||||
[](https://github.com/dptech-corp/Uni-Lab-OS/stargazers)
|
||||
@@ -74,4 +75,4 @@ This project is licensed under GPL-3.0 - see the [LICENSE](LICENSE) file for det
|
||||
|
||||
## Contact Us
|
||||
|
||||
- GitHub Issues: [https://github.com/dptech-corp/Uni-Lab-OS/issues](https://github.com/dptech-corp/Uni-Lab-OS/issues)
|
||||
- GitHub Issues: [https://github.com/dptech-corp/Uni-Lab-OS/issues](https://github.com/dptech-corp/Uni-Lab-OS/issues)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
# Uni-Lab-OS
|
||||
|
||||
<!-- Language switcher -->
|
||||
|
||||
[English](README.md) | **中文**
|
||||
|
||||
[](https://github.com/dptech-corp/Uni-Lab-OS/stargazers)
|
||||
@@ -12,7 +13,7 @@
|
||||
[](https://github.com/dptech-corp/Uni-Lab-OS/issues)
|
||||
[](https://github.com/dptech-corp/Uni-Lab-OS/blob/main/LICENSE)
|
||||
|
||||
Uni-Lab-OS是一个用于实验室自动化的综合平台,旨在连接和控制各种实验设备,实现实验流程的自动化和标准化。
|
||||
Uni-Lab-OS 是一个用于实验室自动化的综合平台,旨在连接和控制各种实验设备,实现实验流程的自动化和标准化。
|
||||
|
||||
## 🏆 比赛
|
||||
|
||||
@@ -34,7 +35,7 @@ Uni-Lab-OS是一个用于实验室自动化的综合平台,旨在连接和控
|
||||
|
||||
## 快速开始
|
||||
|
||||
1. 配置Conda环境
|
||||
1. 配置 Conda 环境
|
||||
|
||||
Uni-Lab-OS 建议使用 `mamba` 管理环境。根据您的操作系统选择适当的环境文件:
|
||||
|
||||
@@ -43,7 +44,7 @@ Uni-Lab-OS 建议使用 `mamba` 管理环境。根据您的操作系统选择适
|
||||
mamba create -n unilab uni-lab::unilabos -c robostack-staging -c conda-forge
|
||||
```
|
||||
|
||||
2. 安装开发版Uni-Lab-OS:
|
||||
2. 安装开发版 Uni-Lab-OS:
|
||||
|
||||
```bash
|
||||
# 克隆仓库
|
||||
@@ -76,4 +77,4 @@ Uni-Lab-OS 使用预构建的 `unilabos_msgs` 进行系统通信。您可以在
|
||||
|
||||
## 联系我们
|
||||
|
||||
- GitHub Issues: [https://github.com/dptech-corp/Uni-Lab-OS/issues](https://github.com/dptech-corp/Uni-Lab-OS/issues)
|
||||
- GitHub Issues: [https://github.com/dptech-corp/Uni-Lab-OS/issues](https://github.com/dptech-corp/Uni-Lab-OS/issues)
|
||||
|
||||
@@ -13,18 +13,16 @@
|
||||
```json
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "PLR_STATION",
|
||||
"name": "PLR_LH_TEST",
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "liquid_handler",
|
||||
"config": {},
|
||||
"data": {},
|
||||
"children": [
|
||||
"deck"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PLR_STATION",
|
||||
"name": "PLR_LH_TEST",
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "liquid_handler",
|
||||
"config": {},
|
||||
"data": {},
|
||||
"children": ["deck"]
|
||||
},
|
||||
{
|
||||
"id": "deck",
|
||||
"name": "deck",
|
||||
@@ -32,12 +30,12 @@
|
||||
"class": null,
|
||||
"parent": "PLR_STATION",
|
||||
"children": [
|
||||
"trash",
|
||||
"trash_core96",
|
||||
"teaching_carrier",
|
||||
"tip_rack",
|
||||
"plate"
|
||||
]
|
||||
"trash",
|
||||
"trash_core96",
|
||||
"teaching_carrier",
|
||||
"tip_rack",
|
||||
"plate"
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
@@ -45,6 +43,7 @@
|
||||
```
|
||||
|
||||
配置文件定义了移液站的组成部分,主要包括:
|
||||
|
||||
- 移液站本体(LiquidHandler)- 设备类型
|
||||
- 移液站携带物料实例(deck)- 物料类型
|
||||
|
||||
@@ -55,7 +54,7 @@
|
||||
使用以下命令启动移液站设备:
|
||||
|
||||
```bash
|
||||
unilab -g test/experiments/plr_test.json --app_bridges ""
|
||||
unilab -g test/experiments/plr_test.json --ak [通过网页获取的ak值] --sk [通过网页获取的sk值]
|
||||
```
|
||||
|
||||
### 2. 执行枪头插入操作
|
||||
@@ -66,35 +65,50 @@ unilab -g test/experiments/plr_test.json --app_bridges ""
|
||||
ros2 action send_goal /devices/PLR_STATION/pick_up_tips unilabos_msgs/action/_liquid_handler_pick_up_tips/LiquidHandlerPickUpTips "{ tip_spots: [ { id: 'tip_rack_tipspot_0_0', name: 'tip_rack_tipspot_0_0', sample_id: null, children: [], parent: 'tip_rack', type: 'device', config: { position: { x: 7.2, y: 68.3, z: -83.5 }, size_x: 9.0, size_y: 9.0, size_z: 0, rotation: { x: 0, y: 0, z: 0, type: 'Rotation' }, category: 'tip_spot', model: null, type: 'TipSpot', prototype_tip: { type: 'HamiltonTip', total_tip_length: 95.1, has_filter: true, maximal_volume: 1065, pickup_method: 'OUT_OF_RACK', tip_size: 'HIGH_VOLUME' } }, data: { tip: { type: 'HamiltonTip', total_tip_length: 95.1, has_filter: true, maximal_volume: 1065, pickup_method: 'OUT_OF_RACK', tip_size: 'HIGH_VOLUME' }, tip_state: { liquids: [], pending_liquids: [], liquid_history: [] }, pending_tip: { type: 'HamiltonTip', total_tip_length: 95.1, has_filter: true, maximal_volume: 1065, pickup_method: 'OUT_OF_RACK', tip_size: 'HIGH_VOLUME' } } } ], use_channels: [ 0 ], offsets: [ { x: 0.0, y: 0.0, z: 0.0 } ] }"
|
||||
```
|
||||
|
||||
此命令会通过ros通信触发移液站执行枪头插入操作,得到如下的PyLabRobot的输出日志。
|
||||
此命令会通过 ros 通信触发移液站执行枪头插入操作,得到如下的 PyLabRobot 的输出日志。
|
||||
|
||||
```log
|
||||
Picking up tips:
|
||||
pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter
|
||||
p0: tip_rack_tipspot_0_0 0.0,0.0,0.0 HamiltonTip 1065 8 95.1 Yes
|
||||
pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter
|
||||
p0: tip_rack_tipspot_0_0 0.0,0.0,0.0 HamiltonTip 1065 8 95.1 Yes
|
||||
```
|
||||
|
||||
也可以登陆网页,给`tip_spots`选择`tip_rack_tipspot_0_0`,`use_channels`为`0`,`offsets`均填写`0`,同样可观察到上面的日志
|
||||
|
||||
## 常见问题
|
||||
|
||||
1. **重复插入枪头不成功**:操作编排应该符合实际操作顺序,可自行通过PyLabRobot进行测试
|
||||
1. **重复插入枪头不成功**:操作编排应该符合实际操作顺序,可自行通过 PyLabRobot 进行测试
|
||||
|
||||
## 移液站支持的操作
|
||||
|
||||
移液站支持多种操作,以下是当前系统支持的操作列表:
|
||||
|
||||
1. **LiquidHandlerAspirate** - 吸液操作
|
||||
2. **LiquidHandlerDispense** - 排液操作
|
||||
3. **LiquidHandlerDiscardTips** - 丢弃枪头
|
||||
4. **LiquidHandlerDropTips** - 卸下枪头
|
||||
5. **LiquidHandlerDropTips96** - 卸下96通道枪头
|
||||
6. **LiquidHandlerMoveLid** - 移动盖子
|
||||
7. **LiquidHandlerMovePlate** - 移动板子
|
||||
8. **LiquidHandlerMoveResource** - 移动资源
|
||||
9. **LiquidHandlerPickUpTips** - 插入枪头
|
||||
10. **LiquidHandlerPickUpTips96** - 插入96通道枪头
|
||||
11. **LiquidHandlerReturnTips** - 归还枪头
|
||||
12. **LiquidHandlerReturnTips96** - 归还96通道枪头
|
||||
13. **LiquidHandlerStamp** - 打印标记
|
||||
14. **LiquidHandlerTransfer** - 液体转移
|
||||
1. **LiquidHandlerProtocolCreation** - 协议创建
|
||||
2. **LiquidHandlerAspirate** - 吸液操作
|
||||
3. **LiquidHandlerDispense** - 排液操作
|
||||
4. **LiquidHandlerDiscardTips** - 丢弃枪头
|
||||
5. **LiquidHandlerDropTips** - 卸下枪头
|
||||
6. **LiquidHandlerDropTips96** - 卸下 96 通道枪头
|
||||
7. **LiquidHandlerMoveLid** - 移动盖子
|
||||
8. **LiquidHandlerMovePlate** - 移动板子
|
||||
9. **LiquidHandlerMoveResource** - 移动资源
|
||||
10. **LiquidHandlerPickUpTips** - 插入枪头
|
||||
11. **LiquidHandlerPickUpTips96** - 插入 96 通道枪头
|
||||
12. **LiquidHandlerReturnTips** - 归还枪头
|
||||
13. **LiquidHandlerReturnTips96** - 归还 96 通道枪头
|
||||
14. **LiquidHandlerSetLiquid** - 设置液体
|
||||
15. **LiquidHandlerSetTipRack** - 设置枪头架
|
||||
16. **LiquidHandlerStamp** - 打印标记
|
||||
17. **LiquidHandlerTransfer** - 液体转移
|
||||
18. **LiquidHandlerSetGroup** - 设置分组
|
||||
19. **LiquidHandlerTransferBiomek** - Biomek 液体转移
|
||||
20. **LiquidHandlerIncubateBiomek** - Biomek 孵育
|
||||
21. **LiquidHandlerMoveBiomek** - Biomek 移动
|
||||
22. **LiquidHandlerOscillateBiomek** - Biomek 振荡
|
||||
23. **LiquidHandlerTransferGroup** - 分组转移
|
||||
24. **LiquidHandlerAdd** - 添加操作
|
||||
25. **LiquidHandlerMix** - 混合操作
|
||||
26. **LiquidHandlerMoveTo** - 移动到指定位置
|
||||
27. **LiquidHandlerRemove** - 移除操作
|
||||
|
||||
这些操作可通过ROS2 Action接口进行调用,以实现复杂的移液流程。
|
||||
这些操作可通过 ROS2 Action 接口进行调用,以实现复杂的移液流程。
|
||||
|
||||
@@ -19,7 +19,7 @@ Uni-Lab 的组态图当前支持 node-link json 和 graphml 格式,其中包
|
||||
对用户来说,“直接操作设备执行单个指令”不是个真实需求,真正的需求是**“执行对实验有意义的单个完整动作”——加入某种液体多少量;萃取分液;洗涤仪器等等。就像实验步骤文字书写的那样。**
|
||||
|
||||
而这些对实验有意义的单个完整动作,**一般需要多个设备的协同**,还依赖于他们的**物理连接关系(管道相连;机械臂可转运)**。
|
||||
于是 Uni-Lab 实现了抽象的“工作站”,即注册表中的 `workstation` 设备(`ProtocolNode`类)来处理编译、规划操作。以泵骨架组成的自动有机实验室为例,设备管道连接关系如下:
|
||||
于是 Uni-Lab 实现了抽象的“工作站”,即注册表中的 `workstation` 设备(`WorkstationNode`类)来处理编译、规划操作。以泵骨架组成的自动有机实验室为例,设备管道连接关系如下:
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -1,26 +1,64 @@
|
||||
## 简单单变量动作函数
|
||||
|
||||
|
||||
### `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`
|
||||
|
||||
@@ -28,7 +66,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `Evaporate`
|
||||
|
||||
@@ -36,7 +74,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `HeatChill`
|
||||
|
||||
@@ -44,7 +82,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `HeatChillStart`
|
||||
|
||||
@@ -52,7 +90,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `HeatChillStop`
|
||||
|
||||
@@ -60,7 +98,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `PumpTransfer`
|
||||
|
||||
@@ -68,7 +106,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `Separate`
|
||||
|
||||
@@ -76,7 +114,7 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `Stir`
|
||||
|
||||
@@ -84,20 +122,179 @@ Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab
|
||||
: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`
|
||||
|
||||
@@ -105,7 +302,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `LiquidHandlerDispense`
|
||||
|
||||
@@ -113,7 +310,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `LiquidHandlerDropTips`
|
||||
|
||||
@@ -121,7 +318,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `LiquidHandlerDropTips96`
|
||||
|
||||
@@ -129,7 +326,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `LiquidHandlerMoveLid`
|
||||
|
||||
@@ -137,7 +334,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `LiquidHandlerMovePlate`
|
||||
|
||||
@@ -145,7 +342,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `LiquidHandlerMoveResource`
|
||||
|
||||
@@ -153,7 +350,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `LiquidHandlerPickUpTips`
|
||||
|
||||
@@ -161,7 +358,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `LiquidHandlerPickUpTips96`
|
||||
|
||||
@@ -169,7 +366,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `LiquidHandlerReturnTips`
|
||||
|
||||
@@ -177,7 +374,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `LiquidHandlerReturnTips96`
|
||||
|
||||
@@ -185,7 +382,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `LiquidHandlerStamp`
|
||||
|
||||
@@ -193,7 +390,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `LiquidHandlerTransfer`
|
||||
|
||||
@@ -201,9 +398,113 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
||||
: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`
|
||||
|
||||
@@ -211,7 +512,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `WorkStationRun`
|
||||
|
||||
@@ -219,12 +520,64 @@ 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
|
||||
@@ -292,7 +645,8 @@ trajectory_msgs/MultiDOFJointTrajectoryPoint multi_dof_error
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `GripperCommand`
|
||||
|
||||
```yaml
|
||||
@@ -310,17 +664,19 @@ bool reached_goal # True iff the gripper position has reached the commanded setp
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `JointTrajectory`
|
||||
|
||||
```yaml
|
||||
trajectory_msgs/JointTrajectory trajectory
|
||||
---
|
||||
---
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `PointHead`
|
||||
|
||||
```yaml
|
||||
@@ -330,12 +686,13 @@ string pointing_frame
|
||||
builtin_interfaces/Duration min_duration
|
||||
float64 max_velocity
|
||||
---
|
||||
|
||||
---
|
||||
float64 pointing_angle_error
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `SingleJointPosition`
|
||||
|
||||
```yaml
|
||||
@@ -343,15 +700,16 @@ float64 position
|
||||
builtin_interfaces/Duration min_duration
|
||||
float64 max_velocity
|
||||
---
|
||||
|
||||
---
|
||||
std_msgs/Header header
|
||||
float64 position
|
||||
float64 velocity
|
||||
float64 error
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `AssistedTeleop`
|
||||
|
||||
```yaml
|
||||
@@ -363,10 +721,10 @@ builtin_interfaces/Duration total_elapsed_time
|
||||
---
|
||||
#feedback
|
||||
builtin_interfaces/Duration current_teleop_duration
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `BackUp`
|
||||
|
||||
```yaml
|
||||
@@ -380,10 +738,10 @@ builtin_interfaces/Duration total_elapsed_time
|
||||
---
|
||||
#feedback definition
|
||||
float32 distance_traveled
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `ComputePathThroughPoses`
|
||||
|
||||
```yaml
|
||||
@@ -398,10 +756,10 @@ nav_msgs/Path path
|
||||
builtin_interfaces/Duration planning_time
|
||||
---
|
||||
#feedback definition
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `ComputePathToPose`
|
||||
|
||||
```yaml
|
||||
@@ -416,10 +774,10 @@ nav_msgs/Path path
|
||||
builtin_interfaces/Duration planning_time
|
||||
---
|
||||
#feedback definition
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `DriveOnHeading`
|
||||
|
||||
```yaml
|
||||
@@ -433,10 +791,10 @@ builtin_interfaces/Duration total_elapsed_time
|
||||
---
|
||||
#feedback definition
|
||||
float32 distance_traveled
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `DummyBehavior`
|
||||
|
||||
```yaml
|
||||
@@ -447,10 +805,10 @@ std_msgs/String command
|
||||
builtin_interfaces/Duration total_elapsed_time
|
||||
---
|
||||
#feedback definition
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `FollowPath`
|
||||
|
||||
```yaml
|
||||
@@ -465,10 +823,10 @@ std_msgs/Empty result
|
||||
#feedback definition
|
||||
float32 distance_to_goal
|
||||
float32 speed
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `FollowWaypoints`
|
||||
|
||||
```yaml
|
||||
@@ -480,10 +838,10 @@ int32[] missed_waypoints
|
||||
---
|
||||
#feedback definition
|
||||
uint32 current_waypoint
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `NavigateThroughPoses`
|
||||
|
||||
```yaml
|
||||
@@ -501,10 +859,10 @@ builtin_interfaces/Duration estimated_time_remaining
|
||||
int16 number_of_recoveries
|
||||
float32 distance_remaining
|
||||
int16 number_of_poses_remaining
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `NavigateToPose`
|
||||
|
||||
```yaml
|
||||
@@ -521,10 +879,10 @@ builtin_interfaces/Duration navigation_time
|
||||
builtin_interfaces/Duration estimated_time_remaining
|
||||
int16 number_of_recoveries
|
||||
float32 distance_remaining
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `SmoothPath`
|
||||
|
||||
```yaml
|
||||
@@ -540,10 +898,10 @@ builtin_interfaces/Duration smoothing_duration
|
||||
bool was_completed
|
||||
---
|
||||
#feedback definition
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `Spin`
|
||||
|
||||
```yaml
|
||||
@@ -556,10 +914,10 @@ builtin_interfaces/Duration total_elapsed_time
|
||||
---
|
||||
#feedback definition
|
||||
float32 angular_distance_traveled
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
### `Wait`
|
||||
|
||||
```yaml
|
||||
@@ -571,7 +929,6 @@ builtin_interfaces/Duration total_elapsed_time
|
||||
---
|
||||
#feedback definition
|
||||
builtin_interfaces/Duration time_left
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
@@ -1,37 +1,142 @@
|
||||
# 添加新动作指令(Action)
|
||||
|
||||
1. 在 `unilabos_msgs/action` 中新建实验操作名和参数列表,如 `MyDeviceCmd.action`。一个 Action 定义由三个部分组成,分别是目标(Goal)、结果(Result)和反馈(Feedback),之间使用 `---` 分隔:
|
||||
本指南将引导你完成添加新动作指令的整个流程,包括编写、在线构建和测试。
|
||||
|
||||
## 1. 编写新的 Action
|
||||
|
||||
### 1.1 创建 Action 文件
|
||||
|
||||
在 `unilabos_msgs/action` 目录中新建实验操作文件,如 `MyDeviceCmd.action`。一个 Action 定义由三个部分组成,分别是目标(Goal)、结果(Result)和反馈(Feedback),之间使用 `---` 分隔:
|
||||
|
||||
```action
|
||||
# 目标(Goal)
|
||||
# 目标(Goal)- 定义动作执行所需的参数
|
||||
string command
|
||||
float64 timeout
|
||||
---
|
||||
# 结果(Result)
|
||||
bool success
|
||||
# 结果(Result)- 定义动作完成后返回的结果
|
||||
bool success # 要求必须包含success,以便回传执行结果
|
||||
string return_info # 要求必须包含return_info,以便回传执行结果
|
||||
... # 其他
|
||||
---
|
||||
# 反馈(Feedback)
|
||||
# 反馈(Feedback)- 定义动作执行过程中的反馈信息
|
||||
float64 progress
|
||||
string status
|
||||
```
|
||||
|
||||
2. 在 `unilabos_msgs/CMakeLists.txt` 中添加新定义的 action
|
||||
### 1.2 更新 CMakeLists.txt
|
||||
|
||||
在 `unilabos_msgs/CMakeLists.txt` 中的 `add_action_files()` 部分添加新定义的 action:
|
||||
|
||||
```cmake
|
||||
add_action_files(
|
||||
FILES
|
||||
MyDeviceCmd.action
|
||||
# 其他已有的 action 文件...
|
||||
)
|
||||
```
|
||||
|
||||
3. 因为在指令集中新建了指令,因此调试时需要编译,并在终端环境中加载临时路径:
|
||||
## 2. 在线构建和测试
|
||||
|
||||
为了简化开发流程并确保构建环境的一致性,我们使用 GitHub Actions 进行在线构建。
|
||||
|
||||
### 2.1 Fork 仓库并创建分支
|
||||
|
||||
1. **Fork 仓库**:在 GitHub 上 fork `Uni-Lab-OS` 仓库到你的个人账户
|
||||
|
||||
2. **Clone 你的 fork**:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/YOUR_USERNAME/Uni-Lab-OS.git
|
||||
cd Uni-Lab-OS
|
||||
```
|
||||
|
||||
3. **创建功能分支**:
|
||||
|
||||
```bash
|
||||
git checkout -b add-my-device-action
|
||||
```
|
||||
|
||||
4. **提交你的更改**:
|
||||
```bash
|
||||
git add unilabos_msgs/action/MyDeviceCmd.action
|
||||
git add unilabos_msgs/CMakeLists.txt
|
||||
git commit -m "Add MyDeviceCmd action for device control"
|
||||
git push origin add-my-device-action
|
||||
```
|
||||
|
||||
### 2.2 触发在线构建
|
||||
|
||||
1. **访问你的 fork 仓库**:在浏览器中打开你的 fork 仓库页面
|
||||
|
||||
2. **手动触发构建**:
|
||||
|
||||
- 点击 "Actions" 标签
|
||||
- 选择 "Multi-Platform Conda Build" 工作流
|
||||
- 点击 "Run workflow" 按钮
|
||||
|
||||
3. **监控构建状态**:
|
||||
- 构建过程大约需要 5-10 分钟
|
||||
- 在 Actions 页面可以实时查看构建日志
|
||||
- 构建完成后,可以下载生成的 conda 包进行测试
|
||||
|
||||
### 2.3 下载和测试构建包
|
||||
|
||||
1. **下载构建产物**:
|
||||
|
||||
- 在构建完成的 Action 页面,找到 "Artifacts" 部分
|
||||
- 下载对应平台的 `conda-package-*` 文件
|
||||
|
||||
2. **本地测试安装**:
|
||||
|
||||
```bash
|
||||
# 解压下载的构建产物
|
||||
unzip conda-package-linux-64.zip # 或其他平台
|
||||
|
||||
# 安装测试包
|
||||
mamba install ./ros-humble-unilabos-msgs-*.conda
|
||||
```
|
||||
|
||||
3. **验证 Action 是否正确添加**:
|
||||
```bash
|
||||
# 检查 action 是否可用
|
||||
ros2 interface show unilabos_msgs/action/MyDeviceCmd
|
||||
```
|
||||
|
||||
## 3. 提交 Pull Request
|
||||
|
||||
测试成功后,向主仓库提交 Pull Request:
|
||||
|
||||
1. **创建 Pull Request**:
|
||||
|
||||
- 在你的 fork 仓库页面,点击 "New Pull Request"
|
||||
- 选择你的功能分支作为源分支
|
||||
- 填写详细的 PR 描述,包括:
|
||||
- 添加的 Action 功能说明
|
||||
- 测试结果
|
||||
- 相关的设备或用例
|
||||
|
||||
2. **等待审核和合并**:
|
||||
- 维护者会审核你的代码
|
||||
- CI/CD 系统会自动运行完整的测试套件
|
||||
- 合并后,新的指令集会自动发布到官方 conda 仓库
|
||||
|
||||
## 4. 使用新的 Action
|
||||
|
||||
如果采用自己构建的action包,可以通过以下命令更新安装:
|
||||
|
||||
```bash
|
||||
cd unilabos_msgs
|
||||
colcon build
|
||||
source ./install/local_setup.sh
|
||||
cd ..
|
||||
mamba remove --force ros-humble-unilabos-msgs
|
||||
mamba config set safety_checks disabled # 如果没有提升版本号,会触发md5与网络上md5不一致,是正常现象,因此通过本指令关闭md5检查
|
||||
mamba install xxx.conda --offline
|
||||
```
|
||||
|
||||
调试成功后,发起 pull request,Uni-Lab 的 CI/CD 系统会自动将新的指令集编译打包,mamba执行升级即可永久生效:
|
||||
## 常见问题
|
||||
|
||||
```bash
|
||||
mamba update ros-humble-unilabos-msgs -c http://quetz.dp.tech:8088/get/unilab -c robostack-humble -c robostack-staging
|
||||
```
|
||||
**Q: 构建失败怎么办?**
|
||||
A: 检查 Actions 日志中的错误信息,通常是语法错误或依赖问题。修复后重新推送代码即可自动触发新的构建。
|
||||
|
||||
**Q: 如何测试特定平台?**
|
||||
A: 在手动触发构建时,在平台选择中只填写你需要的平台,如 `linux-64` 或 `win-64`。
|
||||
|
||||
**Q: 构建包在哪里下载?**
|
||||
A: 在 Actions 页面的构建结果中,查找 "Artifacts" 部分,每个平台都有对应的构建包可供下载。
|
||||
|
||||
@@ -13,36 +13,36 @@ class MockGripper:
|
||||
self._velocity: float = 2.0
|
||||
self._torque: float = 0.0
|
||||
self._status = "Idle"
|
||||
|
||||
|
||||
@property
|
||||
def position(self) -> float:
|
||||
return self._position
|
||||
|
||||
|
||||
@property
|
||||
def velocity(self) -> float:
|
||||
return self._velocity
|
||||
|
||||
|
||||
@property
|
||||
def torque(self) -> float:
|
||||
return self._torque
|
||||
|
||||
|
||||
# 会被自动识别的设备属性,接入 Uni-Lab 时会定时对外广播
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self._status
|
||||
|
||||
|
||||
# 会被自动识别的设备动作,接入 Uni-Lab 时会作为 ActionServer 接受任意控制者的指令
|
||||
@status.setter
|
||||
def status(self, target):
|
||||
self._status = target
|
||||
|
||||
|
||||
# 需要在注册表添加的设备动作,接入 Uni-Lab 时会作为 ActionServer 接受任意控制者的指令
|
||||
def push_to(self, position: float, torque: float, velocity: float = 0.0):
|
||||
self._status = "Running"
|
||||
current_pos = self.position
|
||||
if velocity == 0.0:
|
||||
velocity = self.velocity
|
||||
|
||||
|
||||
move_time = abs(position - current_pos) / velocity
|
||||
for i in range(20):
|
||||
self._position = current_pos + (position - current_pos) / 20 * (i+1)
|
||||
@@ -68,7 +68,7 @@ public class MockGripper
|
||||
public double velocity { get; private set; } = 2.0;
|
||||
public double torque { get; private set; } = 0.0;
|
||||
public string status { get; private set; } = "Idle";
|
||||
|
||||
|
||||
// 需要在注册表添加的设备动作,接入 Uni-Lab 时会作为 ActionServer 接受任意控制者的指令
|
||||
public async Task PushToAsync(double Position, double Torque, double Velocity = 0.0)
|
||||
{
|
||||
@@ -94,107 +94,61 @@ public class MockGripper
|
||||
|
||||
C# 驱动设备在完成注册表后,需要调用 Uni-Lab C# 编译后才能使用,但只需一次。
|
||||
|
||||
## 注册表文件位置
|
||||
## 快速开始:使用注册表编辑器(推荐)
|
||||
|
||||
Uni-Lab 启动时会自动读取默认注册表路径 `unilabos/registry/devices` 下的所有注册设备。您也可以任意维护自己的注册表路径,只需要在 Uni-Lab 启动时使用 `--registry` 参数将路径添加即可。
|
||||
推荐使用 Uni-Lab-OS 自带的可视化编辑器,它能自动分析您的设备驱动并生成大部分配置:
|
||||
|
||||
在 `<path-to-registry>/devices` 中新建一个 yaml 文件,即可开始撰写。您可以将多个设备写到同一个 yaml 文件中。
|
||||
1. 启动 Uni-Lab-OS
|
||||
2. 在浏览器中打开"注册表编辑器"页面
|
||||
3. 选择您的 Python 设备驱动文件
|
||||
4. 点击"分析文件",让系统读取类信息
|
||||
5. 填写基本信息(设备描述、图标等)
|
||||
6. 点击"生成注册表",复制生成的内容
|
||||
7. 保存到 `devices/` 目录下
|
||||
|
||||
## 注册表的结构
|
||||
---
|
||||
|
||||
1. 顶层名称:每个设备的注册表以设备名称开头,例如 `new_device`, `gripper.mock`。
|
||||
1. `class` 字段:定义设备的模块路径和驱动程序语言。
|
||||
1. `status_types` 字段:定义设备定时对 Uni-Lab 实验室内发送的属性名及其类型。
|
||||
1. `action_value_mappings` 字段:定义设备支持的动作及其目标、反馈和结果。
|
||||
1. `schema` 字段:定义设备定时对 Uni-Lab 云端监控发送的属性名及其类型、描述(非必须)
|
||||
## 手动编写注册表(简化版)
|
||||
|
||||
## 创建新的注册表教程
|
||||
如果需要手动编写,只需要提供两个必需字段,系统会自动补全其余内容:
|
||||
|
||||
1. 创建文件
|
||||
在 devices 文件夹中创建一个新的 YAML 文件,例如 `new_device.yaml`。
|
||||
2. 定义设备名称
|
||||
在文件中定义设备的顶层名称,例如:`new_device` 或 `gripper.mock`
|
||||
3. 定义设备的类信息
|
||||
添加设备的模块路径和类型:
|
||||
### 最小配置示例
|
||||
|
||||
```yaml
|
||||
gripper.mock:
|
||||
class: # 定义设备的类信息
|
||||
module: unilabos.devices.gripper.mock:MockGripper
|
||||
type: python # 指定驱动语言为 Python
|
||||
status_types:
|
||||
position: Float64
|
||||
torque: Float64
|
||||
status: String
|
||||
my_device: # 设备唯一标识符
|
||||
class:
|
||||
module: unilabos.devices.your_module.my_device:MyDevice # Python 类路径
|
||||
type: python # 驱动类型
|
||||
```
|
||||
|
||||
4. 定义设备的定时发布属性。注意,对于 Python Class 来说,PROP 是 class 的 `property`,或满足能被 `getattr(cls, PROP)` 或 `cls.get_PROP` 读取到的属性值的对象。
|
||||
### 注册表文件位置
|
||||
|
||||
- 默认路径:`unilabos/registry/devices`
|
||||
- 自定义路径:启动时使用 `--registry` 参数指定
|
||||
- 可将多个设备写在同一个 yaml 文件中
|
||||
|
||||
### 系统自动生成的内容
|
||||
|
||||
系统会自动分析您的 Python 驱动类并生成:
|
||||
|
||||
- `status_types`:从 `get_*` 方法自动识别状态属性
|
||||
- `action_value_mappings`:从类方法自动生成动作映射
|
||||
- `init_param_schema`:从 `__init__` 方法分析初始化参数
|
||||
- `schema`:前端显示用的属性类型定义
|
||||
|
||||
### 完整结构概览
|
||||
|
||||
```yaml
|
||||
status_types:
|
||||
PROP: TYPE
|
||||
```
|
||||
5. 定义设备支持的动作
|
||||
添加设备支持的动作及其目标、反馈和结果:
|
||||
|
||||
```yaml
|
||||
action_value_mappings:
|
||||
set_speed:
|
||||
type: SendCmd
|
||||
goal:
|
||||
command: speed
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
my_device:
|
||||
class:
|
||||
module: unilabos.devices.your_module.my_device:MyDevice
|
||||
type: python
|
||||
status_types: {} # 自动生成
|
||||
action_value_mappings: {} # 自动生成
|
||||
description: '' # 可选:设备描述
|
||||
icon: '' # 可选:设备图标
|
||||
init_param_schema: {} # 自动生成
|
||||
schema: {} # 自动生成
|
||||
```
|
||||
|
||||
在 devices 文件夹中的 YAML 文件中,action_value_mappings 是用来将驱动内的动作函数,映射到 Uni-Lab 标准动作(actions)及其目标参数值(goal)、反馈值(feedback)和结果值(result)的映射规则。若在 Uni-Lab 指令集内找不到符合心意的,请【创建新指令】。
|
||||
|
||||
```yaml
|
||||
action_value_mappings:
|
||||
<action_name>: # <action_name>:动作的名称
|
||||
# start:启动设备或某个功能。
|
||||
# stop:停止设备或某个功能。
|
||||
# set_speed:设置设备的速度。
|
||||
# set_temperature:设置设备的温度。
|
||||
# move_to_position:移动设备到指定位置。
|
||||
# stir:执行搅拌操作。
|
||||
# heatchill:执行加热或冷却操作。
|
||||
# send_nav_task:发送导航任务(例如机器人导航)。
|
||||
# set_timer:设置设备的计时器。
|
||||
# valve_open_cmd:打开阀门。
|
||||
# valve_close_cmd:关闭阀门。
|
||||
# execute_command_from_outer:执行外部命令。
|
||||
# push_to:控制设备推送到某个位置(例如机械爪)。
|
||||
# move_through_points:导航设备通过多个点。
|
||||
|
||||
type: <ActionType> # 动作的类型,表示动作的功能
|
||||
# 根据动作的功能选择合适的类型,请查阅 Uni-Lab 已支持的指令集。
|
||||
|
||||
goal: # 定义动作的目标值映射,表示需要传递给设备的参数。
|
||||
<goal_key>: <mapped_value> #确定设备需要的输入参数,并将其映射到设备的字段。
|
||||
|
||||
feedback: # 定义动作的反馈值映射,表示设备执行动作时返回的实时状态。
|
||||
<feedback_key>: <mapped_value>
|
||||
result: # 定义动作的结果值映射,表示动作完成后返回的最终结果。
|
||||
<result_key>: <mapped_value>
|
||||
```
|
||||
|
||||
6. 定义设备的网页展示属性类型,这部分会被用于在 Uni-Lab 网页端渲染成状态监控
|
||||
添加设备的属性模式,包括属性类型和描述:
|
||||
|
||||
```yaml
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
description: The status of the device
|
||||
speed:
|
||||
type: number
|
||||
description: The speed of the device
|
||||
required:
|
||||
- status
|
||||
- speed
|
||||
additionalProperties: false
|
||||
```
|
||||
详细的注册表编写指南和高级配置,请参考{doc}`yaml 注册表编写指南 <add_yaml>`。
|
||||
|
||||
@@ -1,95 +1,610 @@
|
||||
# yaml注册表编写指南
|
||||
# yaml 注册表编写指南
|
||||
|
||||
`注册表的结构`
|
||||
## 快速开始:使用注册表编辑器
|
||||
|
||||
1. 顶层名称:每个设备的注册表以设备名称开头,例如 new_device。
|
||||
2. class 字段:定义设备的模块路径和类型。
|
||||
3. schema 字段:定义设备的属性模式,包括属性类型、描述和必需字段。
|
||||
4. action_value_mappings 字段:定义设备支持的动作及其目标、反馈和结果。
|
||||
推荐使用 UniLabOS 自带的可视化编辑器,它能帮你自动生成大部分配置,省去手写的麻烦。
|
||||
|
||||
`创建新的注册表教程`
|
||||
1. 创建文件
|
||||
在 devices 文件夹中创建一个新的 YAML 文件,例如 new_device.yaml。
|
||||
### 怎么用编辑器
|
||||
|
||||
2. 定义设备名称
|
||||
在文件中定义设备的顶层名称,例如:new_device
|
||||
1. 启动 UniLabOS
|
||||
2. 在浏览器中打开"注册表编辑器"页面
|
||||
3. 选择你的 Python 设备驱动文件
|
||||
4. 点击"分析文件",让系统读取你的类信息
|
||||
5. 填写一些基本信息(设备描述、图标啥的)
|
||||
6. 点击"生成注册表",复制生成的内容
|
||||
7. 把内容保存到 `devices/` 目录下
|
||||
|
||||
3. 定义设备的类信息
|
||||
添加设备的模块路径和类型:
|
||||
我们为你准备了一个测试驱动,用于在界面上尝试注册表生成,参见目录:test\registry\example_devices.py
|
||||
|
||||
```python
|
||||
new_device: # 定义一个名为 linear_motion.grbl 的设备
|
||||
---
|
||||
|
||||
## 手动编写指南
|
||||
|
||||
class: # 定义设备的类信息
|
||||
module: unilabos.devices_names.new_device:NewDeviceClass # 指定模块路径和类名
|
||||
type: python # 指定类型为 Python 类
|
||||
status_types:
|
||||
```
|
||||
4. 定义设备支持的动作
|
||||
添加设备支持的动作及其目标、反馈和结果:
|
||||
```python
|
||||
action_value_mappings:
|
||||
set_speed:
|
||||
type: SendCmd
|
||||
goal:
|
||||
command: speed
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
```
|
||||
`如何编写action_valve_mappings`
|
||||
1. 在 devices 文件夹中的 YAML 文件中,action_value_mappings 是用来定义设备支持的动作(actions)及其目标值(goal)、反馈值(feedback)和结果值(result)的映射规则。以下是规则和编写方法:
|
||||
```python
|
||||
action_value_mappings:
|
||||
<action_name>: # <action_name>:动作的名称
|
||||
# start:启动设备或某个功能。
|
||||
# stop:停止设备或某个功能。
|
||||
# set_speed:设置设备的速度。
|
||||
# set_temperature:设置设备的温度。
|
||||
# move_to_position:移动设备到指定位置。
|
||||
# stir:执行搅拌操作。
|
||||
# heatchill:执行加热或冷却操作。
|
||||
# send_nav_task:发送导航任务(例如机器人导航)。
|
||||
# set_timer:设置设备的计时器。
|
||||
# valve_open_cmd:打开阀门。
|
||||
# valve_close_cmd:关闭阀门。
|
||||
# execute_command_from_outer:执行外部命令。
|
||||
# push_to:控制设备推送到某个位置(例如机械爪)。
|
||||
# move_through_points:导航设备通过多个点。
|
||||
如果你想自己写 yaml 文件,或者想深入了解结构,查阅下方说明。
|
||||
|
||||
type: <ActionType> # 动作的类型,表示动作的功能
|
||||
# 根据动作的功能选择合适的类型:
|
||||
# SendCmd:发送简单命令。
|
||||
# NavigateThroughPoses:导航动作。
|
||||
# SingleJointPosition:设置单一关节的位置。
|
||||
# Stir:搅拌动作。
|
||||
# HeatChill:加热或冷却动作。
|
||||
## 注册表的基本结构
|
||||
|
||||
goal: # 定义动作的目标值映射,表示需要传递给设备的参数。
|
||||
<goal_key>: <mapped_value> #确定设备需要的输入参数,并将其映射到设备的字段。
|
||||
yaml 注册表就是设备的配置文件,里面定义了设备怎么用、有什么功能。好消息是系统会自动帮你填大部分内容,你只需要写两个必需的东西:设备名和 class 信息。
|
||||
|
||||
feedback: # 定义动作的反馈值映射,表示设备执行动作时返回的实时状态。
|
||||
<feedback_key>: <mapped_value>
|
||||
result: # 定义动作的结果值映射,表示动作完成后返回的最终结果。
|
||||
<result_key>: <mapped_value>
|
||||
### 各字段用途
|
||||
|
||||
| 字段名 | 类型 | 需要手写 | 说明 |
|
||||
| ----------------- | ------ | -------- | ----------------------------------- |
|
||||
| 设备标识符 | string | 是 | 设备的唯一名字,比如 `mock_chiller` |
|
||||
| class | object | 部分 | 设备的核心信息,必须写 |
|
||||
| description | string | 否 | 设备描述,系统默认给空字符串 |
|
||||
| handles | array | 否 | 连接关系,默认是空的 |
|
||||
| icon | string | 否 | 图标路径,默认为空 |
|
||||
| init_param_schema | object | 否 | 初始化参数,系统自动分析生成 |
|
||||
| version | string | 否 | 版本号,默认 "1.0.0" |
|
||||
| category | array | 否 | 设备分类,默认用文件名 |
|
||||
| config_info | array | 否 | 嵌套配置,默认为空 |
|
||||
| file_path | string | 否 | 文件路径,系统自动设置 |
|
||||
| registry_type | string | 否 | 注册表类型,自动设为 "device" |
|
||||
|
||||
### class 字段里有啥
|
||||
|
||||
class 是核心部分,包含这些内容:
|
||||
|
||||
| 字段名 | 类型 | 需要手写 | 说明 |
|
||||
| --------------------- | ------ | -------- | ---------------------------------- |
|
||||
| module | string | 是 | Python 类的路径,必须写 |
|
||||
| type | string | 是 | 驱动类型,一般写 "python" |
|
||||
| status_types | object | 否 | 状态类型,系统自动分析生成 |
|
||||
| action_value_mappings | object | 部分 | 动作配置,系统会自动生成一些基础的 |
|
||||
|
||||
## 怎么创建新的注册表
|
||||
|
||||
### 创建文件
|
||||
|
||||
在 devices 文件夹里新建一个 yaml 文件,比如 `new_device.yaml`。
|
||||
|
||||
### 完整结构是什么样的
|
||||
|
||||
```yaml
|
||||
new_device: # 设备名,要唯一
|
||||
class: # 核心配置
|
||||
action_value_mappings: # 动作配置(后面会详细说)
|
||||
action_name:
|
||||
# 具体的动作设置
|
||||
module: unilabos.devices.your_module.new_device:NewDeviceClass # 你的 Python 类
|
||||
status_types: # 状态类型(系统会自动生成)
|
||||
status: str
|
||||
temperature: float
|
||||
# 其他状态
|
||||
type: python # 驱动类型,一般就是 python
|
||||
|
||||
description: New Device Description # 设备描述
|
||||
handles: [] # 连接关系,通常是空的
|
||||
icon: '' # 图标路径
|
||||
init_param_schema: # 初始化参数(系统会自动生成)
|
||||
config: # 初始化时需要的参数
|
||||
properties:
|
||||
port:
|
||||
default: DEFAULT_PORT
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
data: # 前端显示用的数据类型
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
temperature:
|
||||
type: number
|
||||
required:
|
||||
- status
|
||||
type: object
|
||||
|
||||
version: 0.0.1 # 版本号
|
||||
category:
|
||||
- device_category # 设备类别
|
||||
config_info: [] # 嵌套配置,通常为空
|
||||
```
|
||||
|
||||
6. 定义设备的属性模式
|
||||
添加设备的属性模式,包括属性类型和描述:
|
||||
```python
|
||||
schema:
|
||||
type: object
|
||||
## action_value_mappings 怎么写
|
||||
|
||||
这个部分定义设备能做哪些动作。好消息是系统会自动生成大部分动作,你通常只需要添加一些特殊的自定义动作。
|
||||
|
||||
### 系统自动生成哪些动作
|
||||
|
||||
系统会帮你生成这些:
|
||||
|
||||
1. 以 `auto-` 开头的动作:从你 Python 类的方法自动生成
|
||||
2. 通用的驱动动作:
|
||||
- `_execute_driver_command`:同步执行驱动命令
|
||||
- `_execute_driver_command_async`:异步执行驱动命令
|
||||
|
||||
### 如果要手动定义动作
|
||||
|
||||
如果你需要自定义一些特殊动作,需要这些字段:
|
||||
|
||||
| 字段名 | 需要手写 | 说明 |
|
||||
| ---------------- | -------- | -------------------------------- |
|
||||
| type | 是 | 动作类型,必须指定 |
|
||||
| goal | 是 | 输入参数怎么映射 |
|
||||
| feedback | 否 | 实时反馈,通常为空 |
|
||||
| result | 是 | 结果怎么返回 |
|
||||
| goal_default | 部分 | 参数默认值,ROS 动作会自动生成 |
|
||||
| schema | 部分 | 前端表单配置,ROS 动作会自动生成 |
|
||||
| handles | 否 | 连接关系,默认为空 |
|
||||
| placeholder_keys | 否 | 特殊输入字段配置 |
|
||||
|
||||
### 动作类型有哪些
|
||||
|
||||
| 类型 | 什么时候用 | 系统会自动生成什么 |
|
||||
| ---------------------- | -------------------- | ---------------------- |
|
||||
| UniLabJsonCommand | 自定义同步 JSON 命令 | 啥都不生成 |
|
||||
| UniLabJsonCommandAsync | 自定义异步 JSON 命令 | 啥都不生成 |
|
||||
| ROS 动作类型 | 标准 ROS 动作 | goal_default 和 schema |
|
||||
|
||||
常用的 ROS 动作类型:
|
||||
|
||||
- `SendCmd`:发送简单命令
|
||||
- `NavigateThroughPoses`:导航动作
|
||||
- `SingleJointPosition`:单关节位置控制
|
||||
- `Stir`:搅拌动作
|
||||
- `HeatChill`、`HeatChillStart`:加热冷却动作
|
||||
|
||||
### 复杂一点的例子
|
||||
|
||||
```yaml
|
||||
heat_chill_start:
|
||||
type: HeatChillStart
|
||||
goal:
|
||||
purpose: purpose
|
||||
temp: temp
|
||||
goal_default: # ROS动作会自动生成,你也可以手动覆盖
|
||||
purpose: ''
|
||||
temp: 0.0
|
||||
handles:
|
||||
output:
|
||||
- handler_key: labware
|
||||
label: Labware
|
||||
data_type: resource
|
||||
data_source: handle
|
||||
data_key: liquid
|
||||
placeholder_keys:
|
||||
purpose: unilabos_resources
|
||||
result:
|
||||
status: status
|
||||
success: success
|
||||
# schema 系统会自动生成,不用写
|
||||
```
|
||||
|
||||
### 动作名字怎么起
|
||||
|
||||
根据设备用途来起名字:
|
||||
|
||||
- 启动停止类:`start`、`stop`、`pause`、`resume`
|
||||
- 设置参数类:`set_speed`、`set_temperature`、`set_timer`
|
||||
- 移动控制类:`move_to_position`、`move_through_points`
|
||||
- 功能操作类:`stir`、`heat_chill_start`、`heat_chill_stop`
|
||||
- 开关控制类:`valve_open_cmd`、`valve_close_cmd`、`push_to`
|
||||
- 命令执行类:`send_nav_task`、`execute_command_from_outer`
|
||||
|
||||
### 常用的动作类型
|
||||
|
||||
- `UniLabJsonCommand`:自定义 JSON 命令(不走 ROS)
|
||||
- `UniLabJsonCommandAsync`:异步 JSON 命令(不走 ROS)
|
||||
- `SendCmd`:发送简单命令
|
||||
- `NavigateThroughPoses`:导航相关
|
||||
- `SingleJointPosition`:单关节控制
|
||||
- `Stir`:搅拌
|
||||
- `HeatChill`、`HeatChillStart`:加热冷却
|
||||
- 其他的 ROS 动作类型:看具体的 ROS 服务
|
||||
|
||||
### 示例:完整的动作配置
|
||||
|
||||
```yaml
|
||||
heat_chill_start:
|
||||
type: HeatChillStart
|
||||
goal:
|
||||
purpose: purpose
|
||||
temp: temp
|
||||
goal_default:
|
||||
purpose: ''
|
||||
temp: 0.0
|
||||
handles:
|
||||
output:
|
||||
- handler_key: labware
|
||||
label: Labware
|
||||
data_type: resource
|
||||
data_source: handle
|
||||
data_key: liquid
|
||||
placeholder_keys:
|
||||
purpose: unilabos_resources
|
||||
result:
|
||||
status: status
|
||||
success: success
|
||||
schema:
|
||||
description: '启动加热冷却功能'
|
||||
properties:
|
||||
goal:
|
||||
properties:
|
||||
purpose:
|
||||
type: string
|
||||
description: '用途说明'
|
||||
temp:
|
||||
type: number
|
||||
description: '目标温度'
|
||||
required:
|
||||
- purpose
|
||||
- temp
|
||||
title: HeatChillStart_Goal
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: HeatChillStart
|
||||
type: object
|
||||
feedback: {}
|
||||
```
|
||||
|
||||
## 系统自动生成的字段
|
||||
|
||||
### status_types
|
||||
|
||||
系统会扫描你的 Python 类,从状态方法自动生成这部分:
|
||||
|
||||
```yaml
|
||||
status_types:
|
||||
current_temperature: float # 从 get_current_temperature() 方法来的
|
||||
is_heating: bool # 从 get_is_heating() 方法来的
|
||||
status: str # 从 get_status() 方法来的
|
||||
```
|
||||
|
||||
注意几点:
|
||||
|
||||
- 系统会找所有 `get_` 开头的方法
|
||||
- 类型会自动转成 ROS 类型(比如 `str` 变成 `String`)
|
||||
- 如果类型是 `Any`、`None` 或者不知道的,就默认用 `String`
|
||||
|
||||
### init_param_schema
|
||||
|
||||
这个完全是系统自动生成的,你不用管:
|
||||
|
||||
```yaml
|
||||
init_param_schema:
|
||||
config: # 从你类的 __init__ 方法分析出来的
|
||||
properties:
|
||||
port:
|
||||
type: string
|
||||
default: '/dev/ttyUSB0'
|
||||
baudrate:
|
||||
type: integer
|
||||
default: 9600
|
||||
required: []
|
||||
type: object
|
||||
|
||||
data: # 根据 status_types 生成的前端用的类型
|
||||
properties:
|
||||
current_temperature:
|
||||
type: number
|
||||
is_heating:
|
||||
type: boolean
|
||||
status:
|
||||
type: string
|
||||
description: The status of the device
|
||||
speed:
|
||||
type: number
|
||||
description: The speed of the device
|
||||
required:
|
||||
- status
|
||||
- speed
|
||||
additionalProperties: false
|
||||
type: object
|
||||
```
|
||||
# 写完yaml注册表后需要添加到哪些其他文件?
|
||||
|
||||
生成规则很简单:
|
||||
|
||||
- `config` 部分:看你类的 `__init__` 方法有什么参数,类型和默认值是啥
|
||||
- `data` 部分:根据 `status_types` 生成前端显示用的类型定义
|
||||
|
||||
### 其他自动填充的字段
|
||||
|
||||
```yaml
|
||||
version: '1.0.0' # 默认版本
|
||||
category: ['文件名'] # 用你的 yaml 文件名当类别
|
||||
description: '' # 默认为空,你可以手动改
|
||||
icon: '' # 默认为空,你可以加图标
|
||||
handles: [] # 默认空数组
|
||||
config_info: [] # 默认空数组
|
||||
file_path: '/path/to/file' # 系统自动填文件路径
|
||||
registry_type: 'device' # 自动设为设备类型
|
||||
```
|
||||
|
||||
### handles 字段
|
||||
|
||||
这个是定义设备连接关系的,类似动作里的 handles 一样:
|
||||
|
||||
```yaml
|
||||
handles: # 大多数时候都是空的,除非设备本身需要连接啥
|
||||
- handler_key: device_output
|
||||
label: Device Output
|
||||
data_type: resource
|
||||
data_source: value
|
||||
data_key: default_value
|
||||
```
|
||||
|
||||
### 其他可以配置的字段
|
||||
|
||||
```yaml
|
||||
description: '设备的详细描述' # 写清楚设备是干啥的
|
||||
|
||||
icon: 'device_icon.webp' # 设备图标,文件名(会上传到OSS)
|
||||
|
||||
version: '0.0.1' # 版本号
|
||||
|
||||
category: # 设备分类,前端会用这个分组
|
||||
- 'heating'
|
||||
- 'cooling'
|
||||
- 'temperature_control'
|
||||
|
||||
config_info: # 嵌套配置,如果设备包含子设备
|
||||
- children:
|
||||
- opentrons_24_tuberack_nest_1point5ml_snapcap_A1
|
||||
- other_nested_component
|
||||
```
|
||||
|
||||
## 完整的例子
|
||||
|
||||
这里是一个比较完整的设备配置示例:
|
||||
|
||||
```yaml
|
||||
my_temperature_controller:
|
||||
class:
|
||||
action_value_mappings:
|
||||
heat_start:
|
||||
type: HeatChillStart
|
||||
goal:
|
||||
target_temp: temp
|
||||
vessel: vessel
|
||||
goal_default:
|
||||
target_temp: 25.0
|
||||
vessel: ''
|
||||
handles:
|
||||
output:
|
||||
- handler_key: heated_sample
|
||||
label: Heated Sample
|
||||
data_type: resource
|
||||
data_source: handle
|
||||
data_key: sample
|
||||
placeholder_keys:
|
||||
vessel: unilabos_resources
|
||||
result:
|
||||
status: status
|
||||
success: success
|
||||
schema:
|
||||
description: '启动加热功能'
|
||||
properties:
|
||||
goal:
|
||||
properties:
|
||||
target_temp:
|
||||
type: number
|
||||
description: '目标温度'
|
||||
vessel:
|
||||
type: string
|
||||
description: '容器标识'
|
||||
required:
|
||||
- target_temp
|
||||
- vessel
|
||||
title: HeatStart_Goal
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: HeatStart
|
||||
type: object
|
||||
feedback: {}
|
||||
|
||||
stop:
|
||||
type: UniLabJsonCommand
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
result:
|
||||
status: status
|
||||
schema:
|
||||
description: '停止设备'
|
||||
properties:
|
||||
goal:
|
||||
type: object
|
||||
title: Stop_Goal
|
||||
title: Stop
|
||||
type: object
|
||||
feedback: {}
|
||||
|
||||
module: unilabos.devices.temperature.my_controller:MyTemperatureController
|
||||
status_types:
|
||||
current_temperature: float
|
||||
target_temperature: float
|
||||
is_heating: bool
|
||||
is_cooling: bool
|
||||
status: str
|
||||
vessel: str
|
||||
type: python
|
||||
|
||||
description: '我的温度控制器设备'
|
||||
handles: []
|
||||
icon: 'temperature_controller.webp'
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
port:
|
||||
default: '/dev/ttyUSB0'
|
||||
type: string
|
||||
baudrate:
|
||||
default: 9600
|
||||
type: number
|
||||
required: []
|
||||
type: object
|
||||
data:
|
||||
properties:
|
||||
current_temperature:
|
||||
type: number
|
||||
target_temperature:
|
||||
type: number
|
||||
is_heating:
|
||||
type: boolean
|
||||
is_cooling:
|
||||
type: boolean
|
||||
status:
|
||||
type: string
|
||||
vessel:
|
||||
type: string
|
||||
required:
|
||||
- current_temperature
|
||||
- target_temperature
|
||||
- status
|
||||
type: object
|
||||
|
||||
version: '1.0.0'
|
||||
category:
|
||||
- 'temperature_control'
|
||||
- 'heating'
|
||||
config_info: []
|
||||
```
|
||||
|
||||
## 怎么部署和使用
|
||||
|
||||
### 方法一:用编辑器(推荐)
|
||||
|
||||
1. 先写好你的 Python 驱动类
|
||||
2. 用注册表编辑器自动生成 yaml 配置
|
||||
3. 把生成的文件保存到 `devices/` 目录
|
||||
4. 重启 UniLabOS 就能用了
|
||||
|
||||
### 方法二:手动写(简化版)
|
||||
|
||||
1. 创建最简配置:
|
||||
|
||||
```yaml
|
||||
# devices/my_device.yaml
|
||||
my_device:
|
||||
class:
|
||||
module: unilabos.devices.my_module.my_device:MyDevice
|
||||
type: python
|
||||
```
|
||||
|
||||
2. 启动系统时用 `complete_registry=True` 参数,让系统自动补全
|
||||
|
||||
3. 检查一下生成的配置是不是你想要的
|
||||
|
||||
### Python 驱动类要怎么写
|
||||
|
||||
你的设备类要符合这些要求:
|
||||
|
||||
```python
|
||||
from unilabos.common.device_base import DeviceBase
|
||||
|
||||
class MyDevice(DeviceBase):
|
||||
def __init__(self, config):
|
||||
"""初始化,参数会自动分析到 init_param_schema.config"""
|
||||
super().__init__(config)
|
||||
self.port = config.get('port', '/dev/ttyUSB0')
|
||||
|
||||
# 状态方法(会自动生成到 status_types)
|
||||
def get_status(self):
|
||||
"""返回设备状态"""
|
||||
return "idle"
|
||||
|
||||
def get_temperature(self):
|
||||
"""返回当前温度"""
|
||||
return 25.0
|
||||
|
||||
# 动作方法(会自动生成 auto- 开头的动作)
|
||||
async def start_heating(self, temperature: float):
|
||||
"""开始加热到指定温度"""
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
"""停止操作"""
|
||||
pass
|
||||
```
|
||||
|
||||
### 系统集成
|
||||
|
||||
1. 把 yaml 文件放到 `devices/` 目录下
|
||||
2. 系统启动时会自动扫描并加载设备
|
||||
3. 系统会自动补全所有缺失的字段
|
||||
4. 设备马上就能在前端界面中使用
|
||||
|
||||
### 高级配置
|
||||
|
||||
如果需要特殊设置,可以手动加:
|
||||
|
||||
```yaml
|
||||
my_device:
|
||||
class:
|
||||
module: unilabos.devices.my_module.my_device:MyDevice
|
||||
type: python
|
||||
action_value_mappings:
|
||||
# 自定义动作
|
||||
special_command:
|
||||
type: UniLabJsonCommand
|
||||
goal: {}
|
||||
result: {}
|
||||
|
||||
# 可选的自定义配置
|
||||
description: '我的特殊设备'
|
||||
icon: 'my_device.webp'
|
||||
category: ['temperature', 'heating']
|
||||
```
|
||||
|
||||
## 常见问题怎么排查
|
||||
|
||||
### 设备加载不了
|
||||
|
||||
1. 检查模块路径:确认 `class.module` 路径写对了
|
||||
2. 确认类能导入:看看你的 Python 驱动类能不能正常导入
|
||||
3. 检查语法:用 yaml 验证器看看文件格式对不对
|
||||
4. 查看日志:看 UniLabOS 启动时有没有报错信息
|
||||
|
||||
### 自动生成失败了
|
||||
|
||||
1. 类分析出问题:确认你的类继承了正确的基类
|
||||
2. 方法类型不明确:确保状态方法的返回类型写清楚了
|
||||
3. 导入有问题:检查类能不能被动态导入
|
||||
4. 没开完整注册:确认启用了 `complete_registry=True`
|
||||
|
||||
### 前端显示有问题
|
||||
|
||||
1. 重新生成:删掉旧的 yaml 文件,用编辑器重新生成
|
||||
2. 清除缓存:清除浏览器缓存,重新加载页面
|
||||
3. 检查字段:确认必需的字段(比如 `schema`)都有
|
||||
4. 验证数据:检查 `goal_default` 和 `schema` 的数据类型是不是一致
|
||||
|
||||
### 动作执行出错
|
||||
|
||||
1. 方法名不对:确认动作方法名符合规范(比如 `execute_<action_name>`)
|
||||
2. 参数映射错误:检查 `goal` 字段的参数映射是否正确
|
||||
3. 返回格式不对:确认方法返回值格式符合 `result` 映射
|
||||
4. 没异常处理:在驱动类里加上异常处理
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 开发流程
|
||||
|
||||
1. **优先使用编辑器**:除非有特殊需求,否则优先使用注册表编辑器
|
||||
2. **最小化配置**:手动配置时只定义必要字段,让系统自动生成其他内容
|
||||
3. **增量开发**:先创建基本配置,后续根据需要添加特殊动作
|
||||
|
||||
### 代码规范
|
||||
|
||||
1. **方法命名**:状态方法使用 `get_` 前缀,动作方法使用动词开头
|
||||
2. **类型注解**:为方法参数和返回值添加类型注解
|
||||
3. **文档字符串**:为类和方法添加详细的文档字符串
|
||||
4. **异常处理**:实现完善的错误处理和日志记录
|
||||
|
||||
### 配置管理
|
||||
|
||||
1. **版本控制**:所有 yaml 文件纳入版本控制
|
||||
2. **命名一致性**:设备 ID、文件名、类名保持一致的命名风格
|
||||
3. **定期更新**:定期运行完整注册以更新自动生成的字段
|
||||
4. **备份配置**:在修改前备份重要的手动配置
|
||||
|
||||
### 测试验证
|
||||
|
||||
1. **本地测试**:在本地环境充分测试后再部署
|
||||
2. **渐进部署**:先部署到测试环境,验证无误后再上生产环境
|
||||
3. **监控日志**:密切监控设备加载和运行日志
|
||||
4. **回滚准备**:准备快速回滚机制,以应对紧急情况
|
||||
|
||||
### 性能优化
|
||||
|
||||
1. **按需加载**:只加载实际使用的设备类型
|
||||
2. **缓存利用**:充分利用系统的注册表缓存机制
|
||||
3. **资源管理**:合理管理设备连接和资源占用
|
||||
4. **监控指标**:设置关键性能指标的监控和告警
|
||||
|
||||
378
docs/developer_guide/workstation_architecture.md
Normal file
378
docs/developer_guide/workstation_architecture.md
Normal file
@@ -0,0 +1,378 @@
|
||||
# 工作站基础架构设计文档
|
||||
|
||||
## 1. 整体架构图
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "工作站基础架构"
|
||||
WB[WorkstationBase]
|
||||
WB --> |继承| RPN[ROS2WorkstationNode]
|
||||
WB --> |组合| WCB[WorkstationCommunicationBase]
|
||||
WB --> |组合| MMB[MaterialManagementBase]
|
||||
WB --> |组合| WHS[WorkstationHTTPService]
|
||||
end
|
||||
|
||||
subgraph "通信层实现"
|
||||
WCB --> |实现| PLC[PLCCommunication]
|
||||
WCB --> |实现| SER[SerialCommunication]
|
||||
WCB --> |实现| ETH[EthernetCommunication]
|
||||
end
|
||||
|
||||
subgraph "物料管理实现"
|
||||
MMB --> |实现| PLR[PyLabRobotMaterialManager]
|
||||
MMB --> |实现| BIO[BioyondMaterialManager]
|
||||
MMB --> |实现| SIM[SimpleMaterialManager]
|
||||
end
|
||||
|
||||
subgraph "HTTP服务"
|
||||
WHS --> |处理| LIMS[LIMS协议报送]
|
||||
WHS --> |处理| MAT[物料变更报送]
|
||||
WHS --> |处理| ERR[错误处理报送]
|
||||
end
|
||||
|
||||
subgraph "具体工作站实现"
|
||||
WB --> |继承| WS1[PLCWorkstation]
|
||||
WB --> |继承| WS2[ReportingWorkstation]
|
||||
WB --> |继承| WS3[HybridWorkstation]
|
||||
end
|
||||
|
||||
subgraph "外部系统"
|
||||
EXT1[PLC设备] --> |通信| PLC
|
||||
EXT2[外部工作站] --> |HTTP报送| WHS
|
||||
EXT3[LIMS系统] --> |HTTP报送| WHS
|
||||
EXT4[Bioyond物料系统] --> |查询| BIO
|
||||
end
|
||||
```
|
||||
|
||||
## 2. 类关系图
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class WorkstationBase {
|
||||
<<abstract>>
|
||||
+device_id: str
|
||||
+communication: WorkstationCommunicationBase
|
||||
+material_management: MaterialManagementBase
|
||||
+http_service: WorkstationHTTPService
|
||||
+workflow_status: WorkflowStatus
|
||||
+supported_workflows: Dict
|
||||
|
||||
+_create_communication_module()*
|
||||
+_create_material_management_module()*
|
||||
+_register_supported_workflows()*
|
||||
|
||||
+process_step_finish_report()
|
||||
+process_sample_finish_report()
|
||||
+process_order_finish_report()
|
||||
+process_material_change_report()
|
||||
+handle_external_error()
|
||||
|
||||
+start_workflow()
|
||||
+stop_workflow()
|
||||
+get_workflow_status()
|
||||
+get_device_status()
|
||||
}
|
||||
|
||||
class ROS2WorkstationNode {
|
||||
+sub_devices: Dict
|
||||
+protocol_names: List
|
||||
+execute_single_action()
|
||||
+create_ros_action_server()
|
||||
+initialize_device()
|
||||
}
|
||||
|
||||
class WorkstationCommunicationBase {
|
||||
<<abstract>>
|
||||
+config: CommunicationConfig
|
||||
+is_connected: bool
|
||||
+connect()
|
||||
+disconnect()
|
||||
+start_workflow()*
|
||||
+stop_workflow()*
|
||||
+get_device_status()*
|
||||
+write_register()
|
||||
+read_register()
|
||||
}
|
||||
|
||||
class MaterialManagementBase {
|
||||
<<abstract>>
|
||||
+device_id: str
|
||||
+deck_config: Dict
|
||||
+resource_tracker: DeviceNodeResourceTracker
|
||||
+plr_deck: Deck
|
||||
+find_materials_by_type()
|
||||
+update_material_location()
|
||||
+convert_to_unilab_format()
|
||||
+_create_resource_by_type()*
|
||||
}
|
||||
|
||||
class WorkstationHTTPService {
|
||||
+workstation_instance: WorkstationBase
|
||||
+host: str
|
||||
+port: int
|
||||
+start()
|
||||
+stop()
|
||||
+_handle_step_finish_report()
|
||||
+_handle_material_change_report()
|
||||
}
|
||||
|
||||
class PLCWorkstation {
|
||||
+plc_config: Dict
|
||||
+modbus_client: ModbusTCPClient
|
||||
+_create_communication_module()
|
||||
+_create_material_management_module()
|
||||
+_register_supported_workflows()
|
||||
}
|
||||
|
||||
class ReportingWorkstation {
|
||||
+report_handlers: Dict
|
||||
+_create_communication_module()
|
||||
+_create_material_management_module()
|
||||
+_register_supported_workflows()
|
||||
}
|
||||
|
||||
WorkstationBase --|> ROS2WorkstationNode
|
||||
WorkstationBase *-- WorkstationCommunicationBase
|
||||
WorkstationBase *-- MaterialManagementBase
|
||||
WorkstationBase *-- WorkstationHTTPService
|
||||
|
||||
PLCWorkstation --|> WorkstationBase
|
||||
ReportingWorkstation --|> WorkstationBase
|
||||
|
||||
WorkstationCommunicationBase <|-- PLCCommunication
|
||||
WorkstationCommunicationBase <|-- DummyCommunication
|
||||
|
||||
MaterialManagementBase <|-- PyLabRobotMaterialManager
|
||||
MaterialManagementBase <|-- SimpleMaterialManager
|
||||
```
|
||||
|
||||
## 3. 工作站启动时序图
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant APP as Application
|
||||
participant WS as WorkstationBase
|
||||
participant COMM as CommunicationModule
|
||||
participant MAT as MaterialManager
|
||||
participant HTTP as HTTPService
|
||||
participant ROS as ROS2WorkstationNode
|
||||
|
||||
APP->>WS: 创建工作站实例
|
||||
WS->>ROS: 初始化ROS2WorkstationNode
|
||||
ROS->>ROS: 初始化子设备
|
||||
ROS->>ROS: 设置硬件接口代理
|
||||
|
||||
WS->>COMM: _create_communication_module()
|
||||
COMM->>COMM: 初始化通信配置
|
||||
COMM->>COMM: 建立PLC/串口连接
|
||||
COMM-->>WS: 返回通信模块实例
|
||||
|
||||
WS->>MAT: _create_material_management_module()
|
||||
MAT->>MAT: 创建PyLabRobot Deck
|
||||
MAT->>MAT: 初始化物料资源
|
||||
MAT->>MAT: 注册到ResourceTracker
|
||||
MAT-->>WS: 返回物料管理实例
|
||||
|
||||
WS->>WS: _register_supported_workflows()
|
||||
WS->>WS: _create_workstation_services()
|
||||
WS->>HTTP: _start_http_service()
|
||||
HTTP->>HTTP: 创建HTTP服务器
|
||||
HTTP->>HTTP: 启动监听线程
|
||||
HTTP-->>WS: HTTP服务启动完成
|
||||
|
||||
WS-->>APP: 工作站初始化完成
|
||||
```
|
||||
|
||||
## 4. 工作流执行时序图
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant EXT as ExternalSystem
|
||||
participant WS as WorkstationBase
|
||||
participant COMM as CommunicationModule
|
||||
participant MAT as MaterialManager
|
||||
participant ROS as ROS2WorkstationNode
|
||||
participant DEV as SubDevice
|
||||
|
||||
EXT->>WS: start_workflow(type, params)
|
||||
WS->>WS: 验证工作流类型
|
||||
WS->>COMM: start_workflow(type, params)
|
||||
COMM->>COMM: 发送启动命令到PLC
|
||||
COMM-->>WS: 启动成功
|
||||
|
||||
WS->>WS: 更新workflow_status = RUNNING
|
||||
|
||||
loop 工作流步骤执行
|
||||
WS->>ROS: execute_single_action(device_id, action, params)
|
||||
ROS->>DEV: 发送ROS Action请求
|
||||
DEV->>DEV: 执行设备动作
|
||||
DEV-->>ROS: 返回执行结果
|
||||
ROS-->>WS: 返回动作结果
|
||||
|
||||
WS->>MAT: update_material_location(material_id, location)
|
||||
MAT->>MAT: 更新PyLabRobot资源状态
|
||||
MAT-->>WS: 更新完成
|
||||
end
|
||||
|
||||
WS->>COMM: get_workflow_status()
|
||||
COMM->>COMM: 查询PLC状态寄存器
|
||||
COMM-->>WS: 返回状态信息
|
||||
|
||||
WS->>WS: 更新workflow_status = COMPLETED
|
||||
WS-->>EXT: 工作流执行完成
|
||||
```
|
||||
|
||||
## 5. HTTP报送处理时序图
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant EXT as ExternalWorkstation
|
||||
participant HTTP as HTTPService
|
||||
participant WS as WorkstationBase
|
||||
participant MAT as MaterialManager
|
||||
participant DB as DataStorage
|
||||
|
||||
EXT->>HTTP: POST /report/step_finish
|
||||
HTTP->>HTTP: 解析请求数据
|
||||
HTTP->>HTTP: 验证LIMS协议字段
|
||||
HTTP->>WS: process_step_finish_report(request)
|
||||
|
||||
WS->>WS: 增加接收计数
|
||||
WS->>WS: 记录步骤完成事件
|
||||
WS->>MAT: 更新相关物料状态
|
||||
MAT->>MAT: 更新PyLabRobot资源
|
||||
MAT-->>WS: 更新完成
|
||||
|
||||
WS->>DB: 保存报送记录
|
||||
DB-->>WS: 保存完成
|
||||
|
||||
WS-->>HTTP: 返回处理结果
|
||||
HTTP->>HTTP: 构造HTTP响应
|
||||
HTTP-->>EXT: 200 OK + acknowledgment_id
|
||||
|
||||
Note over EXT,DB: 类似处理sample_finish, order_finish, material_change等报送
|
||||
```
|
||||
|
||||
## 6. 错误处理时序图
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant DEV as Device
|
||||
participant WS as WorkstationBase
|
||||
participant COMM as CommunicationModule
|
||||
participant HTTP as HTTPService
|
||||
participant EXT as ExternalSystem
|
||||
|
||||
DEV->>WS: 设备错误事件
|
||||
WS->>WS: handle_external_error(error_data)
|
||||
WS->>WS: 记录错误历史
|
||||
|
||||
alt 关键错误
|
||||
WS->>COMM: emergency_stop()
|
||||
COMM->>COMM: 发送紧急停止命令
|
||||
WS->>WS: 更新workflow_status = ERROR
|
||||
else 普通错误
|
||||
WS->>WS: 标记动作失败
|
||||
WS->>WS: 触发重试逻辑
|
||||
end
|
||||
|
||||
WS->>HTTP: 记录错误报送
|
||||
HTTP->>EXT: 主动通知错误状态
|
||||
|
||||
WS-->>DEV: 错误处理完成
|
||||
```
|
||||
|
||||
## 7. 典型工作站实现示例
|
||||
|
||||
### 7.1 PLC工作站实现
|
||||
|
||||
```python
|
||||
class PLCWorkstation(WorkstationBase):
|
||||
def _create_communication_module(self):
|
||||
return PLCCommunication(self.communication_config)
|
||||
|
||||
def _create_material_management_module(self):
|
||||
return PyLabRobotMaterialManager(
|
||||
self.device_id,
|
||||
self.deck_config,
|
||||
self.resource_tracker
|
||||
)
|
||||
|
||||
def _register_supported_workflows(self):
|
||||
self.supported_workflows = {
|
||||
"battery_assembly": WorkflowInfo(...),
|
||||
"quality_check": WorkflowInfo(...)
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 报送接收工作站实现
|
||||
|
||||
```python
|
||||
class ReportingWorkstation(WorkstationBase):
|
||||
def _create_communication_module(self):
|
||||
return DummyCommunication(self.communication_config)
|
||||
|
||||
def _create_material_management_module(self):
|
||||
return SimpleMaterialManager(
|
||||
self.device_id,
|
||||
self.deck_config,
|
||||
self.resource_tracker
|
||||
)
|
||||
|
||||
def _register_supported_workflows(self):
|
||||
self.supported_workflows = {
|
||||
"data_collection": WorkflowInfo(...),
|
||||
"report_processing": WorkflowInfo(...)
|
||||
}
|
||||
```
|
||||
|
||||
## 8. 核心接口说明
|
||||
|
||||
### 8.1 必须实现的抽象方法
|
||||
- `_create_communication_module()`: 创建通信模块
|
||||
- `_create_material_management_module()`: 创建物料管理模块
|
||||
- `_register_supported_workflows()`: 注册支持的工作流
|
||||
|
||||
### 8.2 可重写的报送处理方法
|
||||
- `process_step_finish_report()`: 步骤完成处理
|
||||
- `process_sample_finish_report()`: 样本完成处理
|
||||
- `process_order_finish_report()`: 订单完成处理
|
||||
- `process_material_change_report()`: 物料变更处理
|
||||
- `handle_external_error()`: 错误处理
|
||||
|
||||
### 8.3 工作流控制接口
|
||||
- `start_workflow()`: 启动工作流
|
||||
- `stop_workflow()`: 停止工作流
|
||||
- `get_workflow_status()`: 获取状态
|
||||
|
||||
## 9. 配置参数说明
|
||||
|
||||
```python
|
||||
workstation_config = {
|
||||
"communication_config": {
|
||||
"protocol": "modbus_tcp",
|
||||
"host": "192.168.1.100",
|
||||
"port": 502
|
||||
},
|
||||
"deck_config": {
|
||||
"size_x": 1000.0,
|
||||
"size_y": 1000.0,
|
||||
"size_z": 500.0
|
||||
},
|
||||
"http_service_config": {
|
||||
"enabled": True,
|
||||
"host": "127.0.0.1",
|
||||
"port": 8081
|
||||
},
|
||||
"communication_interfaces": {
|
||||
"logical_device_1": CommunicationInterface(...)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这个架构设计支持:
|
||||
1. **灵活的通信方式**: 通过CommunicationBase支持PLC、串口、以太网等
|
||||
2. **多样的物料管理**: 支持PyLabRobot、Bioyond、简单物料系统
|
||||
3. **统一的HTTP报送**: 基于LIMS协议的标准化报送接口
|
||||
4. **完整的工作流控制**: 支持动态和静态工作流
|
||||
5. **强大的错误处理**: 多层次的错误处理和恢复机制
|
||||
@@ -1,82 +1,73 @@
|
||||
# Uni-Lab 配置指南
|
||||
|
||||
Uni-Lab支持通过Python配置文件进行灵活的系统配置。本指南将帮助您理解配置选项并设置您的Uni-Lab环境。
|
||||
Uni-Lab 支持通过 Python 配置文件进行灵活的系统配置。本指南将帮助您理解配置选项并设置您的 Uni-Lab 环境。
|
||||
|
||||
## 配置文件格式
|
||||
|
||||
Uni-Lab支持Python格式的配置文件,它比YAML或JSON提供更多的灵活性,包括支持注释、条件逻辑和复杂数据结构。
|
||||
Uni-Lab 支持 Python 格式的配置文件,它比 YAML 或 JSON 提供更多的灵活性,包括支持注释、条件逻辑和复杂数据结构。
|
||||
|
||||
### 基本配置示例
|
||||
### 默认配置示例
|
||||
|
||||
一个典型的配置文件包含以下部分:
|
||||
首次使用时,系统会自动创建一个基础配置文件 `local_config.py`:
|
||||
|
||||
```python
|
||||
# unilabos的配置文件
|
||||
|
||||
class BasicConfig:
|
||||
ak = "" # 实验室网页给您提供的ak代码,您可以在配置文件中指定,也可以通过运行unilabos时以 --ak 传入,优先按照传入参数解析
|
||||
sk = "" # 实验室网页给您提供的sk代码,您可以在配置文件中指定,也可以通过运行unilabos时以 --sk 传入,优先按照传入参数解析
|
||||
|
||||
|
||||
# WebSocket配置,一般无需调整
|
||||
class WSConfig:
|
||||
reconnect_interval = 5 # 重连间隔(秒)
|
||||
max_reconnect_attempts = 999 # 最大重连次数
|
||||
ping_interval = 30 # ping间隔(秒)
|
||||
```
|
||||
|
||||
### 完整配置示例
|
||||
|
||||
您可以根据需要添加更多配置选项:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python
|
||||
# coding=utf-8
|
||||
"""Uni-Lab 配置文件"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
# 基础配置
|
||||
class BasicConfig:
|
||||
ak = "your_access_key" # 实验室访问密钥
|
||||
sk = "your_secret_key" # 实验室私钥
|
||||
working_dir = "" # 工作目录(通常自动设置)
|
||||
config_path = "" # 配置文件路径(自动设置)
|
||||
is_host_mode = True # 是否为主站模式
|
||||
slave_no_host = False # 从站模式下是否跳过等待主机服务
|
||||
upload_registry = False # 是否上传注册表
|
||||
machine_name = "undefined" # 机器名称(自动获取)
|
||||
vis_2d_enable = False # 是否启用2D可视化
|
||||
enable_resource_load = True # 是否启用资源加载
|
||||
communication_protocol = "websocket" # 通信协议
|
||||
|
||||
# 配置类定义
|
||||
# WebSocket配置
|
||||
class WSConfig:
|
||||
reconnect_interval = 5 # 重连间隔(秒)
|
||||
max_reconnect_attempts = 999 # 最大重连次数
|
||||
ping_interval = 30 # ping间隔(秒)
|
||||
|
||||
class MQConfig:
|
||||
"""MQTT 配置类"""
|
||||
lab_id: str = "YOUR_LAB_ID"
|
||||
# 更多配置...
|
||||
# OSS上传配置
|
||||
class OSSUploadConfig:
|
||||
api_host = "" # API主机地址
|
||||
authorization = "" # 授权信息
|
||||
init_endpoint = "" # 初始化端点
|
||||
complete_endpoint = "" # 完成端点
|
||||
max_retries = 3 # 最大重试次数
|
||||
|
||||
# 其他配置类...
|
||||
```
|
||||
|
||||
## 配置选项说明
|
||||
|
||||
### MQTT配置 (MQConfig)
|
||||
|
||||
MQTT配置用于连接消息队列服务,是Uni-Lab与云端通信的主要方式。
|
||||
|
||||
```python
|
||||
|
||||
class MQConfig:
|
||||
"""MQTT 配置类"""
|
||||
lab_id: str = "7AAEDBEA" # 实验室唯一标识
|
||||
instance_id: str = "mqtt-cn-instance"
|
||||
access_key: str = "your-access-key"
|
||||
secret_key: str = "your-secret-key"
|
||||
group_id: str = "GID_labs"
|
||||
broker_url: str = "mqtt-cn-instance.mqtt.aliyuncs.com"
|
||||
port: int = 8883
|
||||
|
||||
# 可以直接提供证书文件路径
|
||||
ca_file: str = "/path/to/ca.pem" # 相对config.py所在目录的路径
|
||||
cert_file: str = "/path/to/cert.pem" # 相对config.py所在目录的路径
|
||||
key_file: str = "/path/to/key.pem" # 相对config.py所在目录的路径
|
||||
|
||||
# 或者直接提供证书内容
|
||||
ca_content: str = ""
|
||||
cert_content: str = ""
|
||||
key_content: str = ""
|
||||
```
|
||||
|
||||
#### 证书配置
|
||||
|
||||
MQTT连接支持两种方式配置证书:
|
||||
|
||||
1. **文件路径方式**(推荐):指定证书文件的路径,系统会自动读取文件内容
|
||||
2. **直接内容方式**:直接在配置中提供证书内容
|
||||
|
||||
推荐使用文件路径方式,便于证书的更新和管理。
|
||||
|
||||
### HTTP客户端配置 (HTTPConfig)
|
||||
|
||||
即将开放 Uni-Lab 云端实验室。
|
||||
|
||||
### ROS模块配置 (ROSConfig)
|
||||
|
||||
配置ROS消息转换器需要加载的模块:
|
||||
|
||||
```python
|
||||
# HTTP配置
|
||||
class HTTPConfig:
|
||||
remote_addr = "http://127.0.0.1:48197/api/v1" # 远程地址
|
||||
|
||||
# ROS配置
|
||||
class ROSConfig:
|
||||
"""ROS模块配置"""
|
||||
modules = [
|
||||
"std_msgs.msg",
|
||||
"geometry_msgs.msg",
|
||||
@@ -85,25 +76,365 @@ class ROSConfig:
|
||||
"nav2_msgs.action",
|
||||
"unilabos_msgs.msg",
|
||||
"unilabos_msgs.action",
|
||||
] # 需要加载的ROS模块
|
||||
```
|
||||
|
||||
## 命令行参数覆盖配置
|
||||
|
||||
Uni-Lab 允许通过命令行参数覆盖配置文件中的设置,提供更灵活的配置方式。命令行参数的优先级高于配置文件。
|
||||
|
||||
### 支持命令行覆盖的配置项
|
||||
|
||||
以下配置项可以通过命令行参数进行覆盖:
|
||||
|
||||
| 配置类 | 配置字段 | 命令行参数 | 说明 |
|
||||
| ------------- | ----------------- | ------------------- | -------------------------------- |
|
||||
| `BasicConfig` | `ak` | `--ak` | 实验室访问密钥 |
|
||||
| `BasicConfig` | `sk` | `--sk` | 实验室私钥 |
|
||||
| `BasicConfig` | `working_dir` | `--working_dir` | 工作目录路径 |
|
||||
| `BasicConfig` | `is_host_mode` | `--is_slave` | 主站模式(参数为从站模式,取反) |
|
||||
| `BasicConfig` | `slave_no_host` | `--slave_no_host` | 从站模式下跳过等待主机服务 |
|
||||
| `BasicConfig` | `upload_registry` | `--upload_registry` | 启动时上传注册表信息 |
|
||||
| `BasicConfig` | `vis_2d_enable` | `--2d_vis` | 启用 2D 可视化 |
|
||||
| `HTTPConfig` | `remote_addr` | `--addr` | 远程服务地址 |
|
||||
|
||||
### 特殊命令行参数
|
||||
|
||||
除了直接覆盖配置项的参数外,还有一些特殊的命令行参数:
|
||||
|
||||
| 参数 | 说明 |
|
||||
| ------------------- | ------------------------------------ |
|
||||
| `--config` | 指定配置文件路径 |
|
||||
| `--port` | Web 服务端口(不影响配置文件) |
|
||||
| `--disable_browser` | 禁用自动打开浏览器(不影响配置文件) |
|
||||
| `--visual` | 可视化工具选择(不影响配置文件) |
|
||||
| `--skip_env_check` | 跳过环境检查(不影响配置文件) |
|
||||
|
||||
### 配置优先级
|
||||
|
||||
配置项的生效优先级从高到低为:
|
||||
|
||||
1. **命令行参数**:最高优先级
|
||||
2. **环境变量**:中等优先级
|
||||
3. **配置文件**:基础优先级
|
||||
|
||||
### 使用示例
|
||||
|
||||
```bash
|
||||
# 通过命令行覆盖认证信息
|
||||
unilab --ak "new_access_key" --sk "new_secret_key"
|
||||
|
||||
# 覆盖服务器地址
|
||||
unilab --addr "https://custom.server.com/api/v1"
|
||||
|
||||
# 启用从站模式并跳过等待主机
|
||||
unilab --is_slave --slave_no_host
|
||||
|
||||
# 启用上传注册表和2D可视化
|
||||
unilab --upload_registry --2d_vis
|
||||
|
||||
# 组合使用多个覆盖参数
|
||||
unilab --ak "key" --sk "secret" --addr "test" --upload_registry --2d_vis
|
||||
```
|
||||
|
||||
### 预设环境地址
|
||||
|
||||
`--addr` 参数支持以下预设值,会自动转换为对应的完整 URL:
|
||||
|
||||
- `test` → `https://uni-lab.test.bohrium.com/api/v1`
|
||||
- `uat` → `https://uni-lab.uat.bohrium.com/api/v1`
|
||||
- `local` → `http://127.0.0.1:48197/api/v1`
|
||||
- 其他值 → 直接使用作为完整 URL
|
||||
|
||||
## 配置选项详解
|
||||
|
||||
### 基础配置 (BasicConfig)
|
||||
|
||||
基础配置包含了系统运行的核心参数:
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
| ------------------------ | ---- | ------------- | ------------------------------------------ |
|
||||
| `ak` | str | `""` | 实验室访问密钥(必需) |
|
||||
| `sk` | str | `""` | 实验室私钥(必需) |
|
||||
| `working_dir` | str | `""` | 工作目录,通常自动设置 |
|
||||
| `is_host_mode` | bool | `True` | 是否为主站模式 |
|
||||
| `slave_no_host` | bool | `False` | 从站模式下是否跳过等待主机服务 |
|
||||
| `upload_registry` | bool | `False` | 启动时是否上传注册表信息 |
|
||||
| `machine_name` | str | `"undefined"` | 机器名称,自动从 hostname 获取(不可配置) |
|
||||
| `vis_2d_enable` | bool | `False` | 是否启用 2D 可视化 |
|
||||
| `communication_protocol` | str | `"websocket"` | 通信协议,固定为 websocket |
|
||||
|
||||
#### 认证配置
|
||||
|
||||
`ak` 和 `sk` 是必需的认证参数:
|
||||
|
||||
1. **获取方式**:在 [Uni-Lab 官网](https://uni-lab.bohrium.com) 注册实验室后获得
|
||||
2. **配置方式**:
|
||||
- **命令行参数**:`--ak "your_key" --sk "your_secret"`(最高优先级)
|
||||
- **配置文件**:在 `BasicConfig` 类中设置
|
||||
- **环境变量**:`UNILABOS_BASICCONFIG_AK` 和 `UNILABOS_BASICCONFIG_SK`
|
||||
3. **优先级顺序**:命令行参数 > 环境变量 > 配置文件
|
||||
4. **安全注意**:请妥善保管您的密钥信息
|
||||
|
||||
**推荐做法**:
|
||||
|
||||
- 开发环境:使用配置文件
|
||||
- 生产环境:使用环境变量或命令行参数
|
||||
- 临时测试:使用命令行参数
|
||||
|
||||
### WebSocket 配置 (WSConfig)
|
||||
|
||||
WebSocket 是 Uni-Lab 的主要通信方式:
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
| ------------------------ | ---- | ------ | ------------------ |
|
||||
| `reconnect_interval` | int | `5` | 断线重连间隔(秒) |
|
||||
| `max_reconnect_attempts` | int | `999` | 最大重连次数 |
|
||||
| `ping_interval` | int | `30` | 心跳检测间隔(秒) |
|
||||
|
||||
### HTTP 配置 (HTTPConfig)
|
||||
|
||||
HTTP 客户端配置用于与云端服务通信:
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
| ------------- | ---- | --------------------------------- | ------------ |
|
||||
| `remote_addr` | str | `"http://127.0.0.1:48197/api/v1"` | 远程服务地址 |
|
||||
|
||||
**预设环境地址**:
|
||||
|
||||
- 生产环境:`https://uni-lab.bohrium.com/api/v1`
|
||||
- 测试环境:`https://uni-lab.test.bohrium.com/api/v1`
|
||||
- UAT 环境:`https://uni-lab.uat.bohrium.com/api/v1`
|
||||
- 本地环境:`http://127.0.0.1:48197/api/v1`
|
||||
|
||||
### ROS 配置 (ROSConfig)
|
||||
|
||||
配置 ROS 消息转换器需要加载的模块:
|
||||
|
||||
```python
|
||||
class ROSConfig:
|
||||
modules = [
|
||||
"std_msgs.msg", # 标准消息类型
|
||||
"geometry_msgs.msg", # 几何消息类型
|
||||
"control_msgs.msg", # 控制消息类型
|
||||
"control_msgs.action", # 控制动作类型
|
||||
"nav2_msgs.action", # 导航动作类型
|
||||
"unilabos_msgs.msg", # UniLab 自定义消息类型
|
||||
"unilabos_msgs.action", # UniLab 自定义动作类型
|
||||
]
|
||||
```
|
||||
|
||||
您可以根据需要添加其他ROS模块。
|
||||
您可以根据实际使用的设备和功能添加其他 ROS 模块。
|
||||
|
||||
### 其他配置选项
|
||||
### OSS 上传配置 (OSSUploadConfig)
|
||||
|
||||
- **OSSUploadConfig**: 对象存储上传配置
|
||||
对象存储服务配置,用于文件上传功能:
|
||||
|
||||
## 如何使用配置文件
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
| ------------------- | ---- | ------ | -------------------- |
|
||||
| `api_host` | str | `""` | OSS API 主机地址 |
|
||||
| `authorization` | str | `""` | 授权认证信息 |
|
||||
| `init_endpoint` | str | `""` | 上传初始化端点 |
|
||||
| `complete_endpoint` | str | `""` | 上传完成端点 |
|
||||
| `max_retries` | int | `3` | 上传失败最大重试次数 |
|
||||
|
||||
启动Uni-Lab时通过`--config`参数指定配置文件路径:
|
||||
## 环境变量支持
|
||||
|
||||
Uni-Lab 支持通过环境变量覆盖配置文件中的设置。环境变量格式为:
|
||||
|
||||
```
|
||||
UNILABOS_{配置类名}_{字段名}
|
||||
```
|
||||
|
||||
### 环境变量示例
|
||||
|
||||
```bash
|
||||
unilab --config path/to/your/config.py
|
||||
# 设置基础配置
|
||||
export UNILABOS_BASICCONFIG_AK="your_access_key"
|
||||
export UNILABOS_BASICCONFIG_SK="your_secret_key"
|
||||
export UNILABOS_BASICCONFIG_IS_HOST_MODE="true"
|
||||
|
||||
# 设置WebSocket配置
|
||||
export UNILABOS_WSCONFIG_RECONNECT_INTERVAL="10"
|
||||
export UNILABOS_WSCONFIG_MAX_RECONNECT_ATTEMPTS="500"
|
||||
|
||||
# 设置HTTP配置
|
||||
export UNILABOS_HTTPCONFIG_REMOTE_ADDR="https://uni-lab.bohrium.com/api/v1"
|
||||
```
|
||||
|
||||
如果您不涉及多环境开发,可以在unilabos的安装路径中手动添加local_config.py的文件
|
||||
### 环境变量类型转换
|
||||
|
||||
# 启动Uni-Lab
|
||||
python -m unilabos.app.main --config path/to/your/config.py
|
||||
- **布尔值**:`"true"`, `"1"`, `"yes"` → `True`;其他 → `False`
|
||||
- **整数**:自动转换为 `int` 类型
|
||||
- **浮点数**:自动转换为 `float` 类型
|
||||
- **字符串**:保持原值
|
||||
|
||||
## 配置文件使用方法
|
||||
|
||||
### 1. 指定配置文件启动
|
||||
|
||||
```bash
|
||||
# 使用指定配置文件启动
|
||||
unilab --config /path/to/your/config.py
|
||||
```
|
||||
|
||||
### 2. 使用默认配置文件
|
||||
|
||||
如果不指定配置文件,系统会按以下顺序查找:
|
||||
|
||||
1. 环境变量 `UNILABOS_BASICCONFIG_CONFIG_PATH` 指定的路径
|
||||
2. 工作目录下的 `local_config.py`
|
||||
3. 首次使用时会引导创建配置文件
|
||||
|
||||
### 3. 配置文件验证
|
||||
|
||||
系统启动时会自动验证配置文件:
|
||||
|
||||
- **语法检查**:确保 Python 语法正确
|
||||
- **类型检查**:验证配置项类型是否匹配
|
||||
- **必需项检查**:确保 `ak` 和 `sk` 已配置
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 安全配置
|
||||
|
||||
- 不要将包含密钥的配置文件提交到版本控制系统
|
||||
- 使用环境变量或命令行参数在生产环境中配置敏感信息
|
||||
- 定期更换访问密钥
|
||||
- **推荐配置方式**:
|
||||
|
||||
```bash
|
||||
# 生产环境 - 使用环境变量
|
||||
export UNILABOS_BASICCONFIG_AK="your_access_key"
|
||||
export UNILABOS_BASICCONFIG_SK="your_secret_key"
|
||||
unilab
|
||||
|
||||
# 或使用命令行参数
|
||||
unilab --ak "your_access_key" --sk "your_secret_key"
|
||||
```
|
||||
|
||||
### 2. 多环境配置
|
||||
|
||||
为不同环境创建不同的配置文件并结合命令行参数:
|
||||
|
||||
```
|
||||
configs/
|
||||
├── local_config.py # 本地开发
|
||||
├── test_config.py # 测试环境
|
||||
├── prod_config.py # 生产环境
|
||||
└── example_config.py # 示例配置
|
||||
```
|
||||
|
||||
**环境切换示例**:
|
||||
|
||||
```bash
|
||||
# 本地开发环境
|
||||
unilab --config configs/local_config.py --addr local
|
||||
|
||||
# 测试环境
|
||||
unilab --config configs/test_config.py --addr test --upload_registry
|
||||
|
||||
# 生产环境
|
||||
unilab --config configs/prod_config.py --ak "$PROD_AK" --sk "$PROD_SK"
|
||||
```
|
||||
|
||||
### 3. 配置管理
|
||||
|
||||
- 保持配置文件简洁,只包含需要修改的配置项
|
||||
- 为配置项添加注释说明其作用
|
||||
- 定期检查和更新配置文件
|
||||
- **命令行参数优先使用场景**:
|
||||
- 临时测试不同配置
|
||||
- CI/CD 流水线中的动态配置
|
||||
- 不同环境间快速切换
|
||||
- 敏感信息的安全传递
|
||||
|
||||
### 4. 灵活配置策略
|
||||
|
||||
**基础配置文件 + 命令行覆盖**的推荐方式:
|
||||
|
||||
```python
|
||||
# base_config.py - 基础配置
|
||||
class BasicConfig:
|
||||
# 非敏感配置写在文件中
|
||||
is_host_mode = True
|
||||
upload_registry = False
|
||||
vis_2d_enable = False
|
||||
|
||||
class WSConfig:
|
||||
reconnect_interval = 5
|
||||
max_reconnect_attempts = 999
|
||||
ping_interval = 30
|
||||
```
|
||||
|
||||
```bash
|
||||
# 启动时通过命令行覆盖关键参数
|
||||
unilab --config base_config.py \
|
||||
--ak "$AK" \
|
||||
--sk "$SK" \
|
||||
--addr "test" \
|
||||
--upload_registry \
|
||||
--2d_vis
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 1. 配置文件加载失败
|
||||
|
||||
**错误信息**:`[ENV] 配置文件 xxx 不存在`
|
||||
|
||||
**解决方法**:
|
||||
|
||||
- 确认配置文件路径正确
|
||||
- 检查文件权限是否可读
|
||||
- 确保配置文件是 `.py` 格式
|
||||
|
||||
### 2. 语法错误
|
||||
|
||||
**错误信息**:`[ENV] 加载配置文件 xxx 失败`
|
||||
|
||||
**解决方法**:
|
||||
|
||||
- 检查 Python 语法是否正确
|
||||
- 确认类名和字段名拼写正确
|
||||
- 验证缩进是否正确(使用空格而非制表符)
|
||||
|
||||
### 3. 认证失败
|
||||
|
||||
**错误信息**:`后续运行必须拥有一个实验室`
|
||||
|
||||
**解决方法**:
|
||||
|
||||
- 确认 `ak` 和 `sk` 已正确配置
|
||||
- 检查密钥是否有效
|
||||
- 确认网络连接正常
|
||||
|
||||
### 4. 环境变量不生效
|
||||
|
||||
**解决方法**:
|
||||
|
||||
- 确认环境变量名格式正确(`UNILABOS_CLASS_FIELD`)
|
||||
- 检查环境变量是否已正确设置
|
||||
- 重启系统或重新加载环境变量
|
||||
|
||||
### 5. 命令行参数不生效
|
||||
|
||||
**错误现象**:设置了命令行参数但配置没有生效
|
||||
|
||||
**解决方法**:
|
||||
|
||||
- 确认参数名拼写正确(如 `--ak` 而不是 `--access_key`)
|
||||
- 检查参数格式是否正确(布尔参数如 `--is_slave` 不需要值)
|
||||
- 确认参数位置正确(所有参数都应在 `unilab` 之后)
|
||||
- 查看启动日志确认参数是否被正确解析
|
||||
|
||||
### 6. 配置优先级混淆
|
||||
|
||||
**错误现象**:不确定哪个配置生效
|
||||
|
||||
**解决方法**:
|
||||
|
||||
- 记住优先级:命令行参数 > 环境变量 > 配置文件
|
||||
- 使用 `--ak` 和 `--sk` 参数时会看到提示信息
|
||||
- 检查启动日志中的配置加载信息
|
||||
- 临时移除低优先级配置来测试高优先级配置是否生效
|
||||
|
||||
@@ -1,24 +1,43 @@
|
||||
# **Uni-Lab 安装**
|
||||
|
||||
请先 `git clone` 本仓库,随后按照以下步骤安装项目:
|
||||
## 快速开始
|
||||
|
||||
`Uni-Lab` 建议您采用 `mamba` 管理环境。若需从头建立 `Uni-Lab` 的运行依赖环境,请执行
|
||||
1. **配置 Conda 环境**
|
||||
|
||||
Uni-Lab-OS 建议使用 `mamba` 管理环境。创建新的环境:
|
||||
|
||||
```shell
|
||||
mamba env create -f unilabos-<YOUR_OS>.yaml
|
||||
mamba activate unilab
|
||||
mamba create -n unilab uni-lab::unilabos -c robostack-staging -c conda-forge
|
||||
```
|
||||
|
||||
其中 `YOUR_OS` 是您的操作系统,可选值 `win64`, `linux-64`, `osx-64`, `osx-arm64`
|
||||
|
||||
若需将依赖安装进当前环境,请执行
|
||||
2. **安装开发版 Uni-Lab-OS**
|
||||
|
||||
```shell
|
||||
conda env update --file unilabos-<YOUR_OS>.yml
|
||||
# 配置好conda环境后,克隆仓库
|
||||
git clone https://github.com/dptech-corp/Uni-Lab-OS.git -b dev
|
||||
cd Uni-Lab-OS
|
||||
|
||||
# 安装 Uni-Lab-OS
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
随后,可在本仓库安装 `unilabos` 的开发版:
|
||||
3. **安装开发版 ros-humble-unilabos-msgs**
|
||||
|
||||
**卸载老版本:**
|
||||
```shell
|
||||
pip install .
|
||||
conda activate unilab
|
||||
conda remove --force ros-humble-unilabos-msgs
|
||||
```
|
||||
有时相同的安装包版本会由于dev构建得到的md5不一样,触发安全检查,可输入 `config set safety_checks disabled` 来关闭安全检查。
|
||||
|
||||
**安装新版本:**
|
||||
|
||||
访问 https://github.com/dptech-corp/Uni-Lab-OS/actions/workflows/multi-platform-build.yml 选择最新的构建,下载对应平台的压缩包(仅解压一次,得到.conda文件)使用如下指令:
|
||||
```shell
|
||||
conda activate base
|
||||
conda install ros-humble-unilabos-msgs-<version>-<platform>.conda --offline -n <环境名>
|
||||
```
|
||||
|
||||
4. **启动 Uni-Lab 系统**
|
||||
|
||||
请参见{doc}`启动样例 <../boot_examples/index>`或{doc}`启动指南 <launch>`了解详细的启动方法。
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Uni-Lab 启动
|
||||
# Uni-Lab 启动指南
|
||||
|
||||
安装完毕后,可以通过 `unilab` 命令行启动:
|
||||
|
||||
@@ -8,70 +8,240 @@ Start Uni-Lab Edge server.
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
-g GRAPH, --graph GRAPH
|
||||
Physical setup graph.
|
||||
-d DEVICES, --devices DEVICES
|
||||
Devices config file.
|
||||
-r RESOURCES, --resources RESOURCES
|
||||
Resources config file.
|
||||
Physical setup graph file path.
|
||||
-c CONTROLLERS, --controllers CONTROLLERS
|
||||
Controllers config file.
|
||||
Controllers config file path.
|
||||
--registry_path REGISTRY_PATH
|
||||
Path to the registry
|
||||
Path to the registry directory
|
||||
--working_dir WORKING_DIR
|
||||
Path to the working directory
|
||||
--backend {ros,simple,automancer}
|
||||
Choose the backend to run with: 'ros', 'simple', or 'automancer'.
|
||||
--app_bridges APP_BRIDGES [APP_BRIDGES ...]
|
||||
Bridges to connect to. Now support 'mqtt' and 'fastapi'.
|
||||
--without_host Run the backend as slave (without host).
|
||||
--config CONFIG Configuration file path for system settings
|
||||
Bridges to connect to. Now support 'websocket' and 'fastapi'.
|
||||
--is_slave Run the backend as slave node (without host privileges).
|
||||
--slave_no_host Skip waiting for host service in slave mode
|
||||
--upload_registry Upload registry information when starting unilab
|
||||
--use_remote_resource Use remote resources when starting unilab
|
||||
--config CONFIG Configuration file path, supports .py format Python config files
|
||||
--port PORT Port for web service information page
|
||||
--disable_browser Disable opening information page on startup
|
||||
--2d_vis Enable 2D visualization when starting pylabrobot instance
|
||||
--visual {rviz,web,disable}
|
||||
Choose visualization tool: rviz, web, or disable
|
||||
--ak AK Access key for laboratory requests
|
||||
--sk SK Secret key for laboratory requests
|
||||
--addr ADDR Laboratory backend address
|
||||
--skip_env_check Skip environment dependency check on startup
|
||||
--complete_registry Complete registry information
|
||||
```
|
||||
|
||||
## 启动流程详解
|
||||
|
||||
Uni-Lab 的启动过程分为以下几个阶段:
|
||||
|
||||
### 1. 参数解析阶段
|
||||
|
||||
- 解析命令行参数
|
||||
- 处理参数格式转换(支持 dash 和 underscore 格式)
|
||||
|
||||
### 2. 环境检查阶段 (可选)
|
||||
|
||||
- 默认进行环境依赖检查并自动安装必需包
|
||||
- 使用 `--skip_env_check` 可跳过此步骤
|
||||
|
||||
### 3. 配置文件处理阶段
|
||||
|
||||
您可以直接跟随 unilabos 的提示进行,无需查阅本节
|
||||
|
||||
- **工作目录设置**:
|
||||
|
||||
- 如果当前目录以 `unilabos_data` 结尾,则使用当前目录
|
||||
- 否则使用 `当前目录/unilabos_data` 作为工作目录
|
||||
- 可通过 `--working_dir` 指定自定义工作目录
|
||||
|
||||
- **配置文件查找顺序**:
|
||||
1. 使用 `--config` 参数指定的配置文件
|
||||
2. 在工作目录中查找 `local_config.py`
|
||||
3. 首次使用时会引导创建配置文件
|
||||
|
||||
### 4. 服务器地址配置
|
||||
|
||||
支持多种后端环境:
|
||||
|
||||
- `--addr test`:测试环境 (`https://uni-lab.test.bohrium.com/api/v1`)
|
||||
- `--addr uat`:UAT 环境 (`https://uni-lab.uat.bohrium.com/api/v1`)
|
||||
- `--addr local`:本地环境 (`http://127.0.0.1:48197/api/v1`)
|
||||
- 自定义地址:直接指定完整 URL
|
||||
|
||||
### 5. 认证配置
|
||||
|
||||
- **必需参数**:`--ak` 和 `--sk` 必须同时提供
|
||||
- 命令行参数优先于配置文件中的设置
|
||||
- 未提供认证信息会导致启动失败并提示注册实验室
|
||||
|
||||
### 6. 设备图谱加载
|
||||
|
||||
支持两种方式:
|
||||
|
||||
- **本地文件**:使用 `-g` 指定图谱文件(支持 JSON 和 GraphML 格式)
|
||||
- **远程资源**:使用 `--use_remote_resource` 从云端获取
|
||||
|
||||
### 7. 注册表构建
|
||||
|
||||
- 构建设备和资源注册表
|
||||
- 支持自定义注册表路径 (`--registry_path`)
|
||||
- 可选择补全注册表信息 (`--complete_registry`)
|
||||
|
||||
### 8. 设备验证和注册
|
||||
|
||||
- 验证设备连接和端点配置
|
||||
- 自动注册设备到云端服务
|
||||
|
||||
### 9. 通信桥接配置
|
||||
|
||||
- **WebSocket**:实时通信和任务下发
|
||||
- **FastAPI**:HTTP API 服务和物料更新
|
||||
|
||||
### 10. 可视化和服务启动
|
||||
|
||||
- 可选启动可视化工具 (`--visual`)
|
||||
- 启动 Web 信息服务 (默认端口 8002)
|
||||
- 启动后端通信服务
|
||||
|
||||
## 使用配置文件
|
||||
|
||||
Uni-Lab支持使用Python格式的配置文件进行系统设置。通过 `--config` 参数指定配置文件路径:
|
||||
Uni-Lab 支持使用 Python 格式的配置文件进行系统设置。通过 `--config` 参数指定配置文件路径:
|
||||
|
||||
```bash
|
||||
# 使用配置文件启动
|
||||
unilab --config path/to/your/config.py
|
||||
```
|
||||
|
||||
配置文件包含MQTT、HTTP、ROS等系统设置。有关配置文件的详细信息,请参阅[配置指南](configuration.md)。
|
||||
配置文件包含实验室和 WebSocket 连接等设置。有关配置文件的详细信息,请参阅[配置指南](configuration.md)。
|
||||
|
||||
## 初始化信息来源
|
||||
|
||||
启动 Uni-Lab 时,可以选用两种方式之一配置实验室设备、耗材、通信、控制逻辑:
|
||||
启动 Uni-Lab 时,可以选用两种方式之一配置实验室设备:
|
||||
|
||||
### 1. 组态&拓扑图
|
||||
|
||||
使用 `-g` 时,组态&拓扑图应包含实验室所有信息,详见{ref}`graph`。目前支持 graphml 和 node-link json 两种格式。格式可参照 `tests/experiments` 下的启动文件。
|
||||
使用 `-g` 时,组态&拓扑图应包含实验室所有信息,详见{ref}`graph`。目前支持 GraphML 和 node-link JSON 两种格式。格式可参照 `tests/experiments` 下的启动文件。
|
||||
|
||||
### 2. 分别指定设备、耗材、控制逻辑
|
||||
### 2. 分别指定控制逻辑
|
||||
|
||||
分别使用 `-d, -r, -c` 依次传入设备组态配置、耗材列表、控制逻辑。
|
||||
使用 `-c` 传入控制逻辑配置。
|
||||
|
||||
可参照 `devices.json` 和 `resources.json`。
|
||||
|
||||
不管使用哪一种初始化方式,设备/物料字典均需包含 `class` 属性,用于查找注册表信息。默认查找范围都是 Uni-Lab 内部注册表 `unilabos/registry/{devices,device_comms,resources}`。要添加额外的注册表路径,可以使用 `--registry` 加入 `<your-registry-path>/{devices,device_comms,resources}`。
|
||||
不管使用哪一种初始化方式,设备/物料字典均需包含 `class` 属性,用于查找注册表信息。默认查找范围都是 Uni-Lab 内部注册表 `unilabos/registry/{devices,device_comms,resources}`。要添加额外的注册表路径,可以使用 `--registry_path` 加入 `<your-registry-path>/{devices,device_comms,resources}`。
|
||||
|
||||
## 通信中间件 `--backend`
|
||||
|
||||
目前 Uni-Lab 仅支持 ros2 作为通信中间件。
|
||||
目前 Uni-Lab 支持以下通信中间件:
|
||||
|
||||
- **ros** (默认):基于 ROS2 的通信
|
||||
- **simple**:简化通信模式
|
||||
- **automancer**:Automancer 兼容模式
|
||||
|
||||
## 端云桥接 `--app_bridges`
|
||||
|
||||
目前 Uni-Lab 提供 FastAPI (http), MQTT 两种端云通信方式。其中默认 MQTT 负责端对云状态同步和云对端任务下发,FastAPI 负责端对云物料更新。
|
||||
目前 Uni-Lab 提供 WebSocket、FastAPI (http) 两种端云通信方式:
|
||||
|
||||
- **WebSocket**:负责实时通信和任务下发
|
||||
- **FastAPI**:负责端对云物料更新和 HTTP API
|
||||
|
||||
## 分布式组网
|
||||
|
||||
启动 Uni-Lab 时,加入 `--without_host` 将作为从站,不加将作为主站,主站 (host) 持有物料修改权以及对云端的通信。局域网内分别启动的 Uni-Lab 主站/从站将自动组网,互相能访问所有设备状态、传感器信息并发送指令。
|
||||
启动 Uni-Lab 时,加入 `--is_slave` 将作为从站,不加将作为主站:
|
||||
|
||||
- **主站 (host)**:持有物料修改权以及对云端的通信
|
||||
- **从站 (slave)**:无主机权限,可选择跳过等待主机服务 (`--slave_no_host`)
|
||||
|
||||
局域网内分别启动的 Uni-Lab 主站/从站将自动组网,互相能访问所有设备状态、传感器信息并发送指令。
|
||||
|
||||
## 可视化选项
|
||||
|
||||
### 2D 可视化
|
||||
|
||||
使用 `--2d_vis` 在 PyLabRobot 实例启动时同时启动 2D 可视化。
|
||||
|
||||
### 3D 可视化
|
||||
|
||||
通过 `--visual` 参数选择:
|
||||
|
||||
- **rviz**:使用 RViz 进行 3D 可视化
|
||||
- **web**:使用 Web 界面进行可视化
|
||||
- **disable** (默认):禁用可视化
|
||||
|
||||
## 实验室管理
|
||||
|
||||
### 首次使用
|
||||
|
||||
如果是首次使用,系统会:
|
||||
|
||||
1. 提示前往 https://uni-lab.bohrium.com 注册实验室
|
||||
2. 引导创建配置文件
|
||||
3. 设置工作目录
|
||||
|
||||
### 认证设置
|
||||
|
||||
- `--ak`:实验室访问密钥
|
||||
- `--sk`:实验室私钥
|
||||
- 两者必须同时提供才能正常启动
|
||||
|
||||
## 完整启动示例
|
||||
|
||||
以下是一些常用的启动命令示例:
|
||||
|
||||
```bash
|
||||
# 使用配置文件和组态图启动
|
||||
unilab -g path/to/graph.json
|
||||
# 使用组态图启动,上传注册表
|
||||
unilab --ak your_ak --sk your_sk -g path/to/graph.json --upload_registry
|
||||
|
||||
# 使用配置文件和分离的设备/资源文件启动
|
||||
unilab -d devices.json -r resources.json
|
||||
# 使用远程资源启动
|
||||
unilab --ak your_ak --sk your_sk --use_remote_resource
|
||||
|
||||
# 更新注册表
|
||||
unilab --ak your_ak --sk your_sk --complete_registry
|
||||
|
||||
# 启动从站模式
|
||||
unilab --ak your_ak --sk your_sk --is_slave
|
||||
|
||||
# 启用可视化
|
||||
unilab --ak your_ak --sk your_sk --visual web --2d_vis
|
||||
|
||||
# 指定本地信息网页服务端口和禁用自动跳出浏览器
|
||||
unilab --ak your_ak --sk your_sk --port 8080 --disable_browser
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. 认证失败
|
||||
|
||||
如果提示 "后续运行必须拥有一个实验室",请确保:
|
||||
|
||||
- 已在 https://uni-lab.bohrium.com 注册实验室
|
||||
- 正确设置了 `--ak` 和 `--sk` 参数
|
||||
- 配置文件中包含正确的认证信息
|
||||
|
||||
### 2. 配置文件问题
|
||||
|
||||
如果配置文件加载失败:
|
||||
|
||||
- 确保配置文件是 `.py` 格式
|
||||
- 检查配置文件语法是否正确
|
||||
- 首次使用可让系统自动创建示例配置文件
|
||||
|
||||
### 3. 网络连接问题
|
||||
|
||||
如果无法连接到服务器:
|
||||
|
||||
- 检查网络连接
|
||||
- 确认服务器地址是否正确
|
||||
- 尝试使用不同的环境地址(test、uat、local)
|
||||
|
||||
### 4. 设备图谱问题
|
||||
|
||||
如果设备加载失败:
|
||||
|
||||
- 检查图谱文件格式是否正确
|
||||
- 验证设备连接和端点配置
|
||||
- 确保注册表路径正确
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package:
|
||||
name: ros-humble-unilabos-msgs
|
||||
version: 0.10.3
|
||||
version: 0.10.6
|
||||
source:
|
||||
path: ../../unilabos_msgs
|
||||
target_directory: src
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package:
|
||||
name: unilabos
|
||||
version: "0.10.3"
|
||||
version: "0.10.6"
|
||||
|
||||
source:
|
||||
path: ../..
|
||||
|
||||
5
setup.py
5
setup.py
@@ -4,7 +4,7 @@ package_name = 'unilabos'
|
||||
|
||||
setup(
|
||||
name=package_name,
|
||||
version='0.10.3',
|
||||
version='0.10.6',
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=['setuptools'],
|
||||
@@ -16,8 +16,7 @@ setup(
|
||||
tests_require=['pytest'],
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
"unilab = unilabos.app.main:main",
|
||||
"unilab-register = unilabos.app.register:main"
|
||||
"unilab = unilabos.app.main:main"
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
60
test/experiments/dispensing_station_bioyond.json
Normal file
60
test/experiments/dispensing_station_bioyond.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "dispensing_station_bioyond",
|
||||
"name": "dispensing_station_bioyond",
|
||||
"children": [
|
||||
"Bioyond_Dispensing_Deck"
|
||||
],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "dispensing_station.bioyond",
|
||||
"config": {
|
||||
"config": {
|
||||
"api_key": "DE9BDDA0",
|
||||
"api_host": "http://192.168.1.200:44388"
|
||||
},
|
||||
"deck": {
|
||||
"data": {
|
||||
"_resource_child_name": "Bioyond_Dispensing_Deck",
|
||||
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerPreparationStation_Deck"
|
||||
}
|
||||
},
|
||||
"station_config": {
|
||||
"station_type": "dispensing_station",
|
||||
"enable_dispensing_station": true,
|
||||
"enable_reaction_station": false,
|
||||
"station_name": "DispensingStation_001",
|
||||
"description": "Bioyond配液工作站"
|
||||
},
|
||||
"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": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -21,9 +21,9 @@
|
||||
"timeout": 10.0,
|
||||
"axis": "Left",
|
||||
"channel_num": 8,
|
||||
"setup": false,
|
||||
"debug": false,
|
||||
"simulator": false,
|
||||
"setup": true,
|
||||
"debug": true,
|
||||
"simulator": true,
|
||||
"matrix_id": "71593"
|
||||
},
|
||||
"data": {},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
69
test/experiments/reaction_station_bioyond.json
Normal file
69
test/experiments/reaction_station_bioyond.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "reaction_station_bioyond",
|
||||
"name": "reaction_station_bioyond",
|
||||
"parent": null,
|
||||
"children": [
|
||||
"Bioyond_Deck"
|
||||
],
|
||||
"type": "device",
|
||||
"class": "reaction_station.bioyond",
|
||||
"config": {
|
||||
"bioyond_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)": "3a160824-0665-01ed-285a-51ef817a9046",
|
||||
"Liquid_feeding_beaker": "3a16087e-124f-8ddb-8ec1-c2dff09ca784",
|
||||
"Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a"
|
||||
},
|
||||
"material_type_mappings": {
|
||||
"烧杯": "BIOYOND_PolymerStation_1FlaskCarrier",
|
||||
"试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier",
|
||||
"样品板": "BIOYOND_PolymerStation_6VialCarrier"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"sample_id": null,
|
||||
"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": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
69
test/experiments/reaction_station_bioyond_test.json
Normal file
69
test/experiments/reaction_station_bioyond_test.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "reaction_station_bioyond",
|
||||
"name": "reaction_station_bioyond",
|
||||
"parent": null,
|
||||
"children": [
|
||||
"Bioyond_Deck"
|
||||
],
|
||||
"type": "device",
|
||||
"class": "workstation.bioyond",
|
||||
"config": {
|
||||
"bioyond_config": {
|
||||
"api_key": "DE9BDDA0",
|
||||
"api_host": "http://192.168.1.200:44388",
|
||||
"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)": "3a160824-0665-01ed-285a-51ef817a9046",
|
||||
"Liquid_feeding_beaker": "3a16087e-124f-8ddb-8ec1-c2dff09ca784",
|
||||
"Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a"
|
||||
},
|
||||
"material_type_mappings": {
|
||||
"烧杯": "BIOYOND_PolymerStation_1FlaskCarrier",
|
||||
"试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier",
|
||||
"样品板": "BIOYOND_PolymerStation_6VialCarrier"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"sample_id": null,
|
||||
"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": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
588
test/registry/example_devices.py
Normal file
588
test/registry/example_devices.py
Normal file
@@ -0,0 +1,588 @@
|
||||
"""
|
||||
示例设备类文件,用于测试注册表编辑器
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
|
||||
class SmartPumpController:
|
||||
"""
|
||||
智能泵控制器
|
||||
|
||||
支持多种泵送模式,具有高精度流量控制和自动校准功能。
|
||||
适用于实验室自动化系统中的液体处理任务。
|
||||
"""
|
||||
|
||||
def __init__(self, device_id: str = "smart_pump_01", port: str = "/dev/ttyUSB0"):
|
||||
"""
|
||||
初始化智能泵控制器
|
||||
|
||||
Args:
|
||||
device_id: 设备唯一标识符
|
||||
port: 通信端口
|
||||
"""
|
||||
self.device_id = device_id
|
||||
self.port = port
|
||||
self.is_connected = False
|
||||
self.current_flow_rate = 0.0
|
||||
self.total_volume_pumped = 0.0
|
||||
self.calibration_factor = 1.0
|
||||
self.pump_mode = "continuous" # continuous, volume, rate
|
||||
|
||||
def connect_device(self, timeout: int = 10) -> bool:
|
||||
"""
|
||||
连接到泵设备
|
||||
|
||||
Args:
|
||||
timeout: 连接超时时间(秒)
|
||||
|
||||
Returns:
|
||||
bool: 连接是否成功
|
||||
"""
|
||||
# 模拟连接过程
|
||||
self.is_connected = True
|
||||
return True
|
||||
|
||||
def disconnect_device(self) -> bool:
|
||||
"""
|
||||
断开设备连接
|
||||
|
||||
Returns:
|
||||
bool: 断开连接是否成功
|
||||
"""
|
||||
self.is_connected = False
|
||||
self.current_flow_rate = 0.0
|
||||
return True
|
||||
|
||||
def set_flow_rate(self, flow_rate: float, units: str = "ml/min") -> bool:
|
||||
"""
|
||||
设置泵流速
|
||||
|
||||
Args:
|
||||
flow_rate: 流速值
|
||||
units: 流速单位
|
||||
|
||||
Returns:
|
||||
bool: 设置是否成功
|
||||
"""
|
||||
if not self.is_connected:
|
||||
return False
|
||||
|
||||
self.current_flow_rate = flow_rate
|
||||
return True
|
||||
|
||||
async def pump_volume_async(self, volume: float, flow_rate: float) -> Dict[str, Any]:
|
||||
"""
|
||||
异步泵送指定体积的液体
|
||||
|
||||
Args:
|
||||
volume: 目标体积 (mL)
|
||||
flow_rate: 泵送流速 (mL/min)
|
||||
|
||||
Returns:
|
||||
Dict: 包含操作结果的字典
|
||||
"""
|
||||
if not self.is_connected:
|
||||
return {"success": False, "error": "设备未连接"}
|
||||
|
||||
# 计算泵送时间
|
||||
pump_time = (volume / flow_rate) * 60 # 转换为秒
|
||||
|
||||
self.current_flow_rate = flow_rate
|
||||
await asyncio.sleep(min(pump_time, 3.0)) # 模拟泵送过程
|
||||
|
||||
self.total_volume_pumped += volume
|
||||
self.current_flow_rate = 0.0
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"pumped_volume": volume,
|
||||
"actual_time": min(pump_time, 3.0),
|
||||
"total_volume": self.total_volume_pumped,
|
||||
}
|
||||
|
||||
def emergency_stop(self) -> bool:
|
||||
"""
|
||||
紧急停止泵
|
||||
|
||||
Returns:
|
||||
bool: 停止是否成功
|
||||
"""
|
||||
self.current_flow_rate = 0.0
|
||||
return True
|
||||
|
||||
def perform_calibration(self, reference_volume: float, measured_volume: float) -> bool:
|
||||
"""
|
||||
执行泵校准
|
||||
|
||||
Args:
|
||||
reference_volume: 参考体积
|
||||
measured_volume: 实际测量体积
|
||||
|
||||
Returns:
|
||||
bool: 校准是否成功
|
||||
"""
|
||||
if measured_volume > 0:
|
||||
self.calibration_factor = reference_volume / measured_volume
|
||||
return True
|
||||
return False
|
||||
|
||||
# 状态查询方法
|
||||
def get_connection_status(self) -> str:
|
||||
"""获取连接状态"""
|
||||
return "connected" if self.is_connected else "disconnected"
|
||||
|
||||
def get_current_flow_rate(self) -> float:
|
||||
"""获取当前流速 (mL/min)"""
|
||||
return self.current_flow_rate
|
||||
|
||||
def get_total_volume(self) -> float:
|
||||
"""获取累计泵送体积 (mL)"""
|
||||
return self.total_volume_pumped
|
||||
|
||||
def get_calibration_factor(self) -> float:
|
||||
"""获取校准因子"""
|
||||
return self.calibration_factor
|
||||
|
||||
def get_pump_mode(self) -> str:
|
||||
"""获取泵送模式"""
|
||||
return self.pump_mode
|
||||
|
||||
def get_device_status(self) -> Dict[str, Any]:
|
||||
"""获取设备完整状态信息"""
|
||||
return {
|
||||
"device_id": self.device_id,
|
||||
"connected": self.is_connected,
|
||||
"flow_rate": self.current_flow_rate,
|
||||
"total_volume": self.total_volume_pumped,
|
||||
"calibration_factor": self.calibration_factor,
|
||||
"mode": self.pump_mode,
|
||||
"running": self.current_flow_rate > 0,
|
||||
}
|
||||
|
||||
|
||||
class AdvancedTemperatureController:
|
||||
"""
|
||||
高级温度控制器
|
||||
|
||||
支持PID控制、多点温度监控和程序化温度曲线。
|
||||
适用于需要精确温度控制的化学反应和材料处理过程。
|
||||
"""
|
||||
|
||||
def __init__(self, controller_id: str = "temp_controller_01"):
|
||||
"""
|
||||
初始化温度控制器
|
||||
|
||||
Args:
|
||||
controller_id: 控制器ID
|
||||
"""
|
||||
self.controller_id = controller_id
|
||||
self.current_temperature = 25.0
|
||||
self.target_temperature = 25.0
|
||||
self.is_heating = False
|
||||
self.is_cooling = False
|
||||
self.pid_enabled = True
|
||||
self.temperature_history: List[Dict] = []
|
||||
|
||||
def set_target_temperature(self, temperature: float, rate: float = 10.0) -> bool:
|
||||
"""
|
||||
设置目标温度
|
||||
|
||||
Args:
|
||||
temperature: 目标温度 (°C)
|
||||
rate: 升温/降温速率 (°C/min)
|
||||
|
||||
Returns:
|
||||
bool: 设置是否成功
|
||||
"""
|
||||
self.target_temperature = temperature
|
||||
return True
|
||||
|
||||
async def heat_to_temperature_async(
|
||||
self, temperature: float, tolerance: float = 0.5, timeout: int = 600
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
异步加热到指定温度
|
||||
|
||||
Args:
|
||||
temperature: 目标温度 (°C)
|
||||
tolerance: 温度容差 (°C)
|
||||
timeout: 最大等待时间 (秒)
|
||||
|
||||
Returns:
|
||||
Dict: 操作结果
|
||||
"""
|
||||
self.target_temperature = temperature
|
||||
start_temp = self.current_temperature
|
||||
|
||||
if temperature > start_temp:
|
||||
self.is_heating = True
|
||||
elif temperature < start_temp:
|
||||
self.is_cooling = True
|
||||
|
||||
# 模拟温度变化过程
|
||||
steps = min(abs(temperature - start_temp) * 2, 20) # 计算步数
|
||||
step_time = min(timeout / steps if steps > 0 else 1, 2.0) # 每步最多2秒
|
||||
|
||||
for step in range(int(steps)):
|
||||
progress = (step + 1) / steps
|
||||
self.current_temperature = start_temp + (temperature - start_temp) * progress
|
||||
|
||||
# 记录温度历史
|
||||
self.temperature_history.append(
|
||||
{
|
||||
"timestamp": asyncio.get_event_loop().time(),
|
||||
"temperature": self.current_temperature,
|
||||
"target": self.target_temperature,
|
||||
}
|
||||
)
|
||||
|
||||
await asyncio.sleep(step_time)
|
||||
|
||||
# 保持历史记录不超过100条
|
||||
if len(self.temperature_history) > 100:
|
||||
self.temperature_history.pop(0)
|
||||
|
||||
# 最终设置为目标温度
|
||||
self.current_temperature = temperature
|
||||
self.is_heating = False
|
||||
self.is_cooling = False
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"final_temperature": self.current_temperature,
|
||||
"start_temperature": start_temp,
|
||||
"time_taken": steps * step_time,
|
||||
}
|
||||
|
||||
def enable_pid_control(self, kp: float = 1.0, ki: float = 0.1, kd: float = 0.05) -> bool:
|
||||
"""
|
||||
启用PID控制
|
||||
|
||||
Args:
|
||||
kp: 比例增益
|
||||
ki: 积分增益
|
||||
kd: 微分增益
|
||||
|
||||
Returns:
|
||||
bool: 启用是否成功
|
||||
"""
|
||||
self.pid_enabled = True
|
||||
return True
|
||||
|
||||
def run_temperature_program(self, program: List[Dict]) -> bool:
|
||||
"""
|
||||
运行温度程序
|
||||
|
||||
Args:
|
||||
program: 温度程序列表,每个元素包含温度和持续时间
|
||||
|
||||
Returns:
|
||||
bool: 程序启动是否成功
|
||||
"""
|
||||
# 模拟程序启动
|
||||
return True
|
||||
|
||||
# 状态查询方法
|
||||
def get_current_temperature(self) -> float:
|
||||
"""获取当前温度 (°C)"""
|
||||
return round(self.current_temperature, 2)
|
||||
|
||||
def get_target_temperature(self) -> float:
|
||||
"""获取目标温度 (°C)"""
|
||||
return self.target_temperature
|
||||
|
||||
def get_heating_status(self) -> bool:
|
||||
"""获取加热状态"""
|
||||
return self.is_heating
|
||||
|
||||
def get_cooling_status(self) -> bool:
|
||||
"""获取制冷状态"""
|
||||
return self.is_cooling
|
||||
|
||||
def get_pid_status(self) -> bool:
|
||||
"""获取PID控制状态"""
|
||||
return self.pid_enabled
|
||||
|
||||
def get_temperature_history(self) -> List[Dict]:
|
||||
"""获取温度历史记录"""
|
||||
return self.temperature_history[-10:] # 返回最近10条记录
|
||||
|
||||
def get_controller_status(self) -> Dict[str, Any]:
|
||||
"""获取控制器完整状态"""
|
||||
return {
|
||||
"controller_id": self.controller_id,
|
||||
"current_temp": self.current_temperature,
|
||||
"target_temp": self.target_temperature,
|
||||
"is_heating": self.is_heating,
|
||||
"is_cooling": self.is_cooling,
|
||||
"pid_enabled": self.pid_enabled,
|
||||
"history_count": len(self.temperature_history),
|
||||
}
|
||||
|
||||
|
||||
class MultiChannelAnalyzer:
|
||||
"""
|
||||
多通道分析仪
|
||||
|
||||
支持同时监测多个通道的信号,提供实时数据采集和分析功能。
|
||||
常用于光谱分析、电化学测量等应用场景。
|
||||
"""
|
||||
|
||||
def __init__(self, analyzer_id: str = "analyzer_01", channels: int = 8):
|
||||
"""
|
||||
初始化多通道分析仪
|
||||
|
||||
Args:
|
||||
analyzer_id: 分析仪ID
|
||||
channels: 通道数量
|
||||
"""
|
||||
self.analyzer_id = analyzer_id
|
||||
self.channel_count = channels
|
||||
self.channel_data = {i: {"value": 0.0, "unit": "V", "enabled": True} for i in range(channels)}
|
||||
self.is_measuring = False
|
||||
self.sample_rate = 1000 # Hz
|
||||
|
||||
def configure_channel(self, channel: int, enabled: bool = True, unit: str = "V") -> bool:
|
||||
"""
|
||||
配置通道
|
||||
|
||||
Args:
|
||||
channel: 通道编号
|
||||
enabled: 是否启用
|
||||
unit: 测量单位
|
||||
|
||||
Returns:
|
||||
bool: 配置是否成功
|
||||
"""
|
||||
if 0 <= channel < self.channel_count:
|
||||
self.channel_data[channel]["enabled"] = enabled
|
||||
self.channel_data[channel]["unit"] = unit
|
||||
return True
|
||||
return False
|
||||
|
||||
async def start_measurement_async(self, duration: int = 10) -> Dict[str, Any]:
|
||||
"""
|
||||
开始异步测量
|
||||
|
||||
Args:
|
||||
duration: 测量持续时间(秒)
|
||||
|
||||
Returns:
|
||||
Dict: 测量结果
|
||||
"""
|
||||
self.is_measuring = True
|
||||
|
||||
# 模拟数据采集
|
||||
measurements = []
|
||||
for second in range(duration):
|
||||
timestamp = asyncio.get_event_loop().time()
|
||||
frame_data = {}
|
||||
|
||||
for channel in range(self.channel_count):
|
||||
if self.channel_data[channel]["enabled"]:
|
||||
# 模拟传感器数据
|
||||
import random
|
||||
|
||||
value = random.uniform(-5.0, 5.0)
|
||||
frame_data[f"channel_{channel}"] = value
|
||||
self.channel_data[channel]["value"] = value
|
||||
|
||||
measurements.append({"timestamp": timestamp, "data": frame_data})
|
||||
|
||||
await asyncio.sleep(1.0) # 每秒采集一次
|
||||
|
||||
self.is_measuring = False
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"duration": duration,
|
||||
"samples_count": len(measurements),
|
||||
"measurements": measurements[-5:], # 只返回最后5个样本
|
||||
"channels_active": len([ch for ch in self.channel_data.values() if ch["enabled"]]),
|
||||
}
|
||||
|
||||
def stop_measurement(self) -> bool:
|
||||
"""
|
||||
停止测量
|
||||
|
||||
Returns:
|
||||
bool: 停止是否成功
|
||||
"""
|
||||
self.is_measuring = False
|
||||
return True
|
||||
|
||||
def reset_channels(self) -> bool:
|
||||
"""
|
||||
重置所有通道
|
||||
|
||||
Returns:
|
||||
bool: 重置是否成功
|
||||
"""
|
||||
for channel in self.channel_data:
|
||||
self.channel_data[channel]["value"] = 0.0
|
||||
return True
|
||||
|
||||
# 状态查询方法
|
||||
def get_measurement_status(self) -> bool:
|
||||
"""获取测量状态"""
|
||||
return self.is_measuring
|
||||
|
||||
def get_channel_count(self) -> int:
|
||||
"""获取通道数量"""
|
||||
return self.channel_count
|
||||
|
||||
def get_sample_rate(self) -> float:
|
||||
"""获取采样率 (Hz)"""
|
||||
return self.sample_rate
|
||||
|
||||
def get_channel_values(self) -> Dict[int, float]:
|
||||
"""获取所有通道的当前值"""
|
||||
return {ch: data["value"] for ch, data in self.channel_data.items() if data["enabled"]}
|
||||
|
||||
def get_enabled_channels(self) -> List[int]:
|
||||
"""获取已启用的通道列表"""
|
||||
return [ch for ch, data in self.channel_data.items() if data["enabled"]]
|
||||
|
||||
def get_analyzer_status(self) -> Dict[str, Any]:
|
||||
"""获取分析仪完整状态"""
|
||||
return {
|
||||
"analyzer_id": self.analyzer_id,
|
||||
"channel_count": self.channel_count,
|
||||
"is_measuring": self.is_measuring,
|
||||
"sample_rate": self.sample_rate,
|
||||
"active_channels": len(self.get_enabled_channels()),
|
||||
"channel_data": self.channel_data,
|
||||
}
|
||||
|
||||
|
||||
class AutomatedDispenser:
|
||||
"""
|
||||
自动分配器
|
||||
|
||||
精确控制固体和液体材料的分配,支持多种分配模式和容器管理。
|
||||
集成称重功能,确保分配精度和重现性。
|
||||
"""
|
||||
|
||||
def __init__(self, dispenser_id: str = "dispenser_01"):
|
||||
"""
|
||||
初始化自动分配器
|
||||
|
||||
Args:
|
||||
dispenser_id: 分配器ID
|
||||
"""
|
||||
self.dispenser_id = dispenser_id
|
||||
self.is_ready = True
|
||||
self.current_position = {"x": 0.0, "y": 0.0, "z": 0.0}
|
||||
self.dispensed_total = 0.0
|
||||
self.container_capacity = 1000.0 # mL
|
||||
self.precision_mode = True
|
||||
|
||||
def move_to_position(self, x: float, y: float, z: float) -> bool:
|
||||
"""
|
||||
移动到指定位置
|
||||
|
||||
Args:
|
||||
x: X坐标 (mm)
|
||||
y: Y坐标 (mm)
|
||||
z: Z坐标 (mm)
|
||||
|
||||
Returns:
|
||||
bool: 移动是否成功
|
||||
"""
|
||||
self.current_position = {"x": x, "y": y, "z": z}
|
||||
return True
|
||||
|
||||
async def dispense_liquid_async(self, volume: float, container_id: str, viscosity: str = "low") -> Dict[str, Any]:
|
||||
"""
|
||||
异步分配液体
|
||||
|
||||
Args:
|
||||
volume: 分配体积 (mL)
|
||||
container_id: 容器ID
|
||||
viscosity: 液体粘度等级
|
||||
|
||||
Returns:
|
||||
Dict: 分配结果
|
||||
"""
|
||||
if not self.is_ready:
|
||||
return {"success": False, "error": "设备未就绪"}
|
||||
|
||||
if volume <= 0:
|
||||
return {"success": False, "error": "体积必须大于0"}
|
||||
|
||||
# 模拟分配过程
|
||||
dispense_time = volume * 0.1 # 每mL需要0.1秒
|
||||
if viscosity == "high":
|
||||
dispense_time *= 2 # 高粘度液体需要更长时间
|
||||
|
||||
await asyncio.sleep(min(dispense_time, 5.0)) # 最多等待5秒
|
||||
|
||||
self.dispensed_total += volume
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"dispensed_volume": volume,
|
||||
"container_id": container_id,
|
||||
"actual_time": min(dispense_time, 5.0),
|
||||
"total_dispensed": self.dispensed_total,
|
||||
}
|
||||
|
||||
def clean_dispenser(self, wash_volume: float = 5.0) -> bool:
|
||||
"""
|
||||
清洗分配器
|
||||
|
||||
Args:
|
||||
wash_volume: 清洗液体积 (mL)
|
||||
|
||||
Returns:
|
||||
bool: 清洗是否成功
|
||||
"""
|
||||
# 模拟清洗过程
|
||||
return True
|
||||
|
||||
def calibrate_volume(self, target_volume: float) -> bool:
|
||||
"""
|
||||
校准分配体积
|
||||
|
||||
Args:
|
||||
target_volume: 校准目标体积 (mL)
|
||||
|
||||
Returns:
|
||||
bool: 校准是否成功
|
||||
"""
|
||||
# 模拟校准过程
|
||||
return True
|
||||
|
||||
# 状态查询方法
|
||||
def get_ready_status(self) -> bool:
|
||||
"""获取就绪状态"""
|
||||
return self.is_ready
|
||||
|
||||
def get_current_position(self) -> Dict[str, float]:
|
||||
"""获取当前位置坐标"""
|
||||
return self.current_position.copy()
|
||||
|
||||
def get_dispensed_total(self) -> float:
|
||||
"""获取累计分配体积 (mL)"""
|
||||
return self.dispensed_total
|
||||
|
||||
def get_container_capacity(self) -> float:
|
||||
"""获取容器容量 (mL)"""
|
||||
return self.container_capacity
|
||||
|
||||
def get_precision_mode(self) -> bool:
|
||||
"""获取精密模式状态"""
|
||||
return self.precision_mode
|
||||
|
||||
def get_dispenser_status(self) -> Dict[str, Any]:
|
||||
"""获取分配器完整状态"""
|
||||
return {
|
||||
"dispenser_id": self.dispenser_id,
|
||||
"ready": self.is_ready,
|
||||
"position": self.current_position,
|
||||
"dispensed_total": self.dispensed_total,
|
||||
"capacity": self.container_capacity,
|
||||
"precision_mode": self.precision_mode,
|
||||
}
|
||||
198
test/resources/bioyond_materials.json
Normal file
198
test/resources/bioyond_materials.json
Normal file
@@ -0,0 +1,198 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "3a1c67a9-aed7-b94d-9e24-bfdf10c8baa9",
|
||||
"typeName": "烧杯",
|
||||
"code": "0006-00160",
|
||||
"barCode": "",
|
||||
"name": "ODA",
|
||||
"quantity": 120000.00000000000000000000000,
|
||||
"lockQuantity": 695374.00000000000000000000000,
|
||||
"unit": "微升",
|
||||
"status": 1,
|
||||
"isUse": false,
|
||||
"locations": [
|
||||
{
|
||||
"id": "3a14aa17-0d49-11d7-a6e1-f236b3e5e5a3",
|
||||
"whid": "3a14aa17-0d49-dce4-486e-4b5c85c8b366",
|
||||
"whName": "堆栈1",
|
||||
"code": "0001-0001",
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1,
|
||||
"quantity": 0
|
||||
}
|
||||
],
|
||||
"detail": []
|
||||
},
|
||||
{
|
||||
"id": "3a1c67a9-aed9-1ade-5fe1-cc04b24b171c",
|
||||
"typeName": "烧杯",
|
||||
"code": "0006-00161",
|
||||
"barCode": "",
|
||||
"name": "MPDA",
|
||||
"quantity": 120000.00000000000000000000000,
|
||||
"lockQuantity": 681618.00000000000000000000000,
|
||||
"unit": "",
|
||||
"status": 1,
|
||||
"isUse": false,
|
||||
"locations": [
|
||||
{
|
||||
"id": "3a14aa17-0d49-4bc5-8836-517b75473f5f",
|
||||
"whid": "3a14aa17-0d49-dce4-486e-4b5c85c8b366",
|
||||
"whName": "堆栈1",
|
||||
"code": "0001-0002",
|
||||
"x": 1,
|
||||
"y": 2,
|
||||
"z": 1,
|
||||
"quantity": 0
|
||||
}
|
||||
],
|
||||
"detail": []
|
||||
},
|
||||
{
|
||||
"id": "3a1c67a9-aed9-2864-6783-2cee4e701ba6",
|
||||
"typeName": "试剂瓶",
|
||||
"code": "0004-00041",
|
||||
"barCode": "",
|
||||
"name": "NMP",
|
||||
"quantity": 300000.00000000000000000000000,
|
||||
"lockQuantity": 380000.00000000000000000000000,
|
||||
"unit": "微升",
|
||||
"status": 1,
|
||||
"isUse": false,
|
||||
"locations": [
|
||||
{
|
||||
"id": "3a14aa3b-9fab-adac-7b9c-e1ee446b51d5",
|
||||
"whid": "3a14aa3b-9fab-9d8e-d1a7-828f01f51f0c",
|
||||
"whName": "站内试剂存放堆栈",
|
||||
"code": "0003-0001",
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1,
|
||||
"quantity": 0
|
||||
}
|
||||
],
|
||||
"detail": []
|
||||
},
|
||||
{
|
||||
"id": "3a1c67a9-aed9-32c7-5809-3ba1b8db1aa1",
|
||||
"typeName": "试剂瓶",
|
||||
"code": "0004-00042",
|
||||
"barCode": "",
|
||||
"name": "PGME",
|
||||
"quantity": 300000.00000000000000000000000,
|
||||
"lockQuantity": 337892.00000000000000000000000,
|
||||
"unit": "",
|
||||
"status": 1,
|
||||
"isUse": false,
|
||||
"locations": [
|
||||
{
|
||||
"id": "3a14aa3b-9fab-ca72-febc-b7c304476c78",
|
||||
"whid": "3a14aa3b-9fab-9d8e-d1a7-828f01f51f0c",
|
||||
"whName": "站内试剂存放堆栈",
|
||||
"code": "0003-0002",
|
||||
"x": 1,
|
||||
"y": 2,
|
||||
"z": 1,
|
||||
"quantity": 0
|
||||
}
|
||||
],
|
||||
"detail": []
|
||||
},
|
||||
{
|
||||
"id": "3a1c68c8-0574-d748-725e-97a2e549f085",
|
||||
"typeName": "样品板",
|
||||
"code": "0001-00004",
|
||||
"barCode": "",
|
||||
"name": "0917",
|
||||
"quantity": 1.0000000000000000000000000000,
|
||||
"lockQuantity": 4.0000000000000000000000000000,
|
||||
"unit": "块",
|
||||
"status": 1,
|
||||
"isUse": false,
|
||||
"locations": [
|
||||
{
|
||||
"id": "3a14aa17-0d49-f49c-6b66-b27f185a3b32",
|
||||
"whid": "3a14aa17-0d49-dce4-486e-4b5c85c8b366",
|
||||
"whName": "堆栈1",
|
||||
"code": "0001-0009",
|
||||
"x": 2,
|
||||
"y": 1,
|
||||
"z": 1,
|
||||
"quantity": 0
|
||||
}
|
||||
],
|
||||
"detail": [
|
||||
{
|
||||
"id": "3a1c68c8-0574-69a1-9858-4637e0193451",
|
||||
"detailMaterialId": "3a1c68c8-0574-3630-bd42-bbf3623c5208",
|
||||
"code": null,
|
||||
"name": "SIDA",
|
||||
"quantity": "300000",
|
||||
"lockQuantity": "4",
|
||||
"unit": "微升",
|
||||
"x": 1,
|
||||
"y": 2,
|
||||
"z": 1,
|
||||
"associateId": null
|
||||
},
|
||||
{
|
||||
"id": "3a1c68c8-0574-8d51-3191-a31f5be421e5",
|
||||
"detailMaterialId": "3a1c68c8-0574-3b20-9ad7-90755f123d53",
|
||||
"code": null,
|
||||
"name": "BTDA-2",
|
||||
"quantity": "300000",
|
||||
"lockQuantity": "4",
|
||||
"unit": "微升",
|
||||
"x": 2,
|
||||
"y": 2,
|
||||
"z": 1,
|
||||
"associateId": null
|
||||
},
|
||||
{
|
||||
"id": "3a1c68c8-0574-da80-735b-53ae2197a360",
|
||||
"detailMaterialId": "3a1c68c8-0574-f2e4-33b3-90d813567939",
|
||||
"code": null,
|
||||
"name": "BTDA-DD",
|
||||
"quantity": "300000",
|
||||
"lockQuantity": "28",
|
||||
"unit": "微升",
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"z": 1,
|
||||
"associateId": null
|
||||
},
|
||||
{
|
||||
"id": "3a1c68c8-0574-e717-1b1b-99891f875455",
|
||||
"detailMaterialId": "3a1c68c8-0574-a0ef-e636-68cdc98960e2",
|
||||
"code": null,
|
||||
"name": "BTDA-3",
|
||||
"quantity": "300000",
|
||||
"lockQuantity": "4",
|
||||
"unit": "微升",
|
||||
"x": 2,
|
||||
"y": 3,
|
||||
"z": 1,
|
||||
"associateId": null
|
||||
},
|
||||
{
|
||||
"id": "3a1c68c8-0574-e9bd-6cca-5e261b4f89cb",
|
||||
"detailMaterialId": "3a1c68c8-0574-9d11-5115-283e8e5510b1",
|
||||
"code": null,
|
||||
"name": "BTDA-1",
|
||||
"quantity": "300000",
|
||||
"lockQuantity": "4",
|
||||
"unit": "微升",
|
||||
"x": 2,
|
||||
"y": 1,
|
||||
"z": 1,
|
||||
"associateId": null
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"code": 1,
|
||||
"message": "",
|
||||
"timestamp": 1758560573511
|
||||
}
|
||||
48
test/resources/test_bottle_carrier.py
Normal file
48
test/resources/test_bottle_carrier.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import pytest
|
||||
|
||||
from unilabos.resources.bioyond.bottle_carriers import BIOYOND_Electrolyte_6VialCarrier, BIOYOND_Electrolyte_1BottleCarrier
|
||||
from unilabos.resources.bioyond.bottles import BIOYOND_PolymerStation_Solid_Vial, BIOYOND_PolymerStation_Solution_Beaker, BIOYOND_PolymerStation_Reagent_Bottle
|
||||
|
||||
|
||||
def test_bottle_carrier() -> "BottleCarrier":
|
||||
print("创建载架...")
|
||||
|
||||
# 创建6瓶载架
|
||||
bottle_carrier = BIOYOND_Electrolyte_6VialCarrier("powder_carrier_01")
|
||||
print(f"6瓶载架: {bottle_carrier.name}, 位置数: {len(bottle_carrier.sites)}")
|
||||
|
||||
# 创建1烧杯载架
|
||||
beaker_carrier = BIOYOND_Electrolyte_1BottleCarrier("solution_carrier_01")
|
||||
print(f"1烧杯载架: {beaker_carrier.name}, 位置数: {len(beaker_carrier.sites)}")
|
||||
|
||||
# 创建瓶子和烧杯
|
||||
powder_bottle = BIOYOND_PolymerStation_Solid_Vial("powder_bottle_01")
|
||||
solution_beaker = BIOYOND_PolymerStation_Solution_Beaker("solution_beaker_01")
|
||||
reagent_bottle = BIOYOND_PolymerStation_Reagent_Bottle("reagent_bottle_01")
|
||||
|
||||
print(f"\n创建的物料:")
|
||||
print(f"粉末瓶: {powder_bottle.name} - {powder_bottle.diameter}mm x {powder_bottle.height}mm, {powder_bottle.max_volume}μL")
|
||||
print(f"溶液烧杯: {solution_beaker.name} - {solution_beaker.diameter}mm x {solution_beaker.height}mm, {solution_beaker.max_volume}μL")
|
||||
print(f"试剂瓶: {reagent_bottle.name} - {reagent_bottle.diameter}mm x {reagent_bottle.height}mm, {reagent_bottle.max_volume}μL")
|
||||
|
||||
# 测试放置容器
|
||||
print(f"\n测试放置容器...")
|
||||
|
||||
# 通过载架的索引操作来放置容器
|
||||
# bottle_carrier[0] = powder_bottle # 放置粉末瓶到第一个位置
|
||||
print(f"粉末瓶已放置到6瓶载架的位置 0")
|
||||
|
||||
# beaker_carrier[0] = solution_beaker # 放置烧杯到第一个位置
|
||||
print(f"溶液烧杯已放置到1烧杯载架的位置 0")
|
||||
|
||||
# 验证放置结果
|
||||
print(f"\n验证放置结果:")
|
||||
bottle_at_0 = bottle_carrier[0].resource
|
||||
beaker_at_0 = beaker_carrier[0].resource
|
||||
|
||||
if bottle_at_0:
|
||||
print(f"位置 0 的瓶子: {bottle_at_0.name}")
|
||||
if beaker_at_0:
|
||||
print(f"位置 0 的烧杯: {beaker_at_0.name}")
|
||||
|
||||
print("\n载架设置完成!")
|
||||
35
test/resources/test_converter_bioyond.py
Normal file
35
test/resources/test_converter_bioyond.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import pytest
|
||||
import json
|
||||
import os
|
||||
|
||||
from unilabos.resources.graphio import resource_bioyond_to_plr
|
||||
from unilabos.registry.registry import lab_registry
|
||||
|
||||
from unilabos.resources.bioyond.decks import BIOYOND_PolymerReactionStation_Deck
|
||||
|
||||
lab_registry.setup()
|
||||
|
||||
|
||||
type_mapping = {
|
||||
"烧杯": "BIOYOND_PolymerStation_1FlaskCarrier",
|
||||
"试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier",
|
||||
"样品板": "BIOYOND_PolymerStation_6VialCarrier",
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def bioyond_materials() -> list[dict]:
|
||||
print("加载 BioYond 物料数据...")
|
||||
print(os.getcwd())
|
||||
with open("bioyond_materials.json", "r", encoding="utf-8") as f:
|
||||
data = json.load(f)["data"]
|
||||
print(f"加载了 {len(data)} 条物料数据")
|
||||
return data
|
||||
|
||||
|
||||
def test_bioyond_to_plr(bioyond_materials) -> list[dict]:
|
||||
deck = BIOYOND_PolymerReactionStation_Deck("test_deck")
|
||||
print("将 BioYond 物料数据转换为 PLR 格式...")
|
||||
output = resource_bioyond_to_plr(bioyond_materials, type_mapping=type_mapping, deck=deck)
|
||||
print(deck.summary())
|
||||
print([resource.serialize() for resource in output])
|
||||
print([resource.serialize_all_state() for resource in output])
|
||||
@@ -1,254 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Dummy2 机械臂接入 UniLab 系统测试脚本
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import yaml
|
||||
import json
|
||||
|
||||
def test_device_model_files():
|
||||
"""测试设备模型文件是否完整"""
|
||||
print("=== 测试设备模型文件 ===")
|
||||
|
||||
device_path = "/Users/dp/Documents/Uni-Lab-OS/unilabos/device_mesh/devices/dummy2_robot"
|
||||
|
||||
required_files = [
|
||||
"macro_device.xacro",
|
||||
"dummy2_robot.json",
|
||||
"config/joint_limits.yaml",
|
||||
"config/default_kinematics.yaml",
|
||||
"config/physical_parameters.yaml",
|
||||
"config/visual_parameters.yaml"
|
||||
]
|
||||
|
||||
required_meshes = [
|
||||
"meshes/base_link.stl",
|
||||
"meshes/J1_1.stl",
|
||||
"meshes/J2_1.stl",
|
||||
"meshes/J3_1.stl",
|
||||
"meshes/J4_1.stl",
|
||||
"meshes/J5_1.stl",
|
||||
"meshes/J6_1.stl",
|
||||
"meshes/camera_1.stl"
|
||||
]
|
||||
|
||||
all_files = required_files + required_meshes
|
||||
missing_files = []
|
||||
|
||||
for file_path in all_files:
|
||||
full_path = os.path.join(device_path, file_path)
|
||||
if not os.path.exists(full_path):
|
||||
missing_files.append(file_path)
|
||||
else:
|
||||
print(f"✅ {file_path}")
|
||||
|
||||
if missing_files:
|
||||
print(f"❌ 缺少文件: {missing_files}")
|
||||
return False
|
||||
else:
|
||||
print("✅ 所有模型文件都存在")
|
||||
return True
|
||||
|
||||
def test_driver_file():
|
||||
"""测试驱动文件"""
|
||||
print("\n=== 测试驱动文件 ===")
|
||||
|
||||
driver_path = "/Users/dp/Documents/Uni-Lab-OS/unilabos/devices/ros_dev/moveit_interface.py"
|
||||
|
||||
if not os.path.exists(driver_path):
|
||||
print(f"❌ 驱动文件不存在: {driver_path}")
|
||||
return False
|
||||
|
||||
try:
|
||||
# 尝试导入驱动类
|
||||
sys.path.insert(0, os.path.dirname(driver_path))
|
||||
from unilabos.devices.ros_dev.moveit_interface import MoveitInterface
|
||||
print("✅ 驱动文件存在且可导入")
|
||||
|
||||
# 检查必要的方法
|
||||
required_methods = [
|
||||
'__init__',
|
||||
'post_init',
|
||||
'check_tf_update_actions',
|
||||
'resource_manager',
|
||||
'wait_for_resource_action',
|
||||
'moveit_joint_task',
|
||||
'moveit_task'
|
||||
]
|
||||
|
||||
missing_methods = []
|
||||
for method in required_methods:
|
||||
if not hasattr(MoveitInterface, method):
|
||||
missing_methods.append(method)
|
||||
|
||||
if missing_methods:
|
||||
print(f"❌ 驱动类缺少方法: {missing_methods}")
|
||||
return False
|
||||
else:
|
||||
print("✅ 驱动类包含所有必要方法")
|
||||
return True
|
||||
|
||||
except ImportError as e:
|
||||
print(f"❌ 驱动文件导入失败: {e}")
|
||||
return False
|
||||
|
||||
def test_registry_config():
|
||||
"""测试注册表配置"""
|
||||
print("\n=== 测试注册表配置 ===")
|
||||
|
||||
registry_path = "/Users/dp/Documents/Uni-Lab-OS/unilabos/registry/devices/robot_arm.yaml"
|
||||
|
||||
if not os.path.exists(registry_path):
|
||||
print(f"❌ 注册表文件不存在: {registry_path}")
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(registry_path, 'r', encoding='utf-8') as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
if 'robotic_arm.Dummy2' not in config:
|
||||
print("❌ 注册表中没有找到 robotic_arm.Dummy2 配置")
|
||||
return False
|
||||
|
||||
dummy2_config = config['robotic_arm.Dummy2']
|
||||
|
||||
# 检查必要的配置项
|
||||
required_keys = [
|
||||
'category',
|
||||
'class',
|
||||
'description',
|
||||
'init_param_schema',
|
||||
'model',
|
||||
'version'
|
||||
]
|
||||
|
||||
missing_keys = []
|
||||
for key in required_keys:
|
||||
if key not in dummy2_config:
|
||||
missing_keys.append(key)
|
||||
|
||||
if missing_keys:
|
||||
print(f"❌ Dummy2配置缺少字段: {missing_keys}")
|
||||
return False
|
||||
|
||||
# 检查模块路径
|
||||
module_path = dummy2_config.get('class', {}).get('module')
|
||||
if module_path != 'unilabos.devices.ros_dev.moveit_interface:MoveitInterface':
|
||||
print(f"❌ 模块路径不正确: {module_path}")
|
||||
return False
|
||||
|
||||
# 检查动作定义
|
||||
actions = dummy2_config.get('class', {}).get('action_value_mappings', {})
|
||||
required_actions = [
|
||||
'auto-check_tf_update_actions',
|
||||
'auto-post_init',
|
||||
'auto-resource_manager',
|
||||
'auto-wait_for_resource_action',
|
||||
'auto-moveit_joint_task',
|
||||
'auto-moveit_task',
|
||||
'pick_and_place'
|
||||
]
|
||||
|
||||
missing_actions = []
|
||||
for action in required_actions:
|
||||
if action not in actions:
|
||||
missing_actions.append(action)
|
||||
|
||||
if missing_actions:
|
||||
print(f"❌ 缺少动作定义: {missing_actions}")
|
||||
return False
|
||||
|
||||
print("✅ 注册表配置完整且正确")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 注册表配置检查失败: {e}")
|
||||
return False
|
||||
|
||||
def test_can2eth_connectivity():
|
||||
"""测试CAN2ETH连接(可选)"""
|
||||
print("\n=== 测试CAN2ETH连接 ===")
|
||||
|
||||
try:
|
||||
import socket
|
||||
import struct
|
||||
|
||||
# 尝试连接CAN2ETH网关
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.settimeout(2.0)
|
||||
|
||||
can2eth_host = "192.168.8.88"
|
||||
can2eth_port = 8080
|
||||
|
||||
# 发送ping命令
|
||||
ping_cmd = struct.pack('>B', 0xFF)
|
||||
sock.sendto(ping_cmd, (can2eth_host, can2eth_port))
|
||||
|
||||
try:
|
||||
data, addr = sock.recvfrom(1024)
|
||||
if len(data) > 0:
|
||||
print(f"✅ CAN2ETH网关 {can2eth_host}:{can2eth_port} 连接成功")
|
||||
return True
|
||||
except socket.timeout:
|
||||
print(f"⚠️ CAN2ETH网关 {can2eth_host}:{can2eth_port} 无响应(可能未启动)")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ CAN2ETH连接测试失败: {e}")
|
||||
return False
|
||||
finally:
|
||||
if 'sock' in locals():
|
||||
sock.close()
|
||||
|
||||
def main():
|
||||
"""主测试函数"""
|
||||
print("🤖 Dummy2 机械臂接入 UniLab 系统测试")
|
||||
print("=" * 50)
|
||||
|
||||
tests = [
|
||||
("设备模型文件", test_device_model_files),
|
||||
("驱动文件", test_driver_file),
|
||||
("注册表配置", test_registry_config),
|
||||
("CAN2ETH连接", test_can2eth_connectivity)
|
||||
]
|
||||
|
||||
results = []
|
||||
|
||||
for test_name, test_func in tests:
|
||||
try:
|
||||
result = test_func()
|
||||
results.append((test_name, result))
|
||||
except Exception as e:
|
||||
print(f"❌ {test_name}测试异常: {e}")
|
||||
results.append((test_name, False))
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("📊 测试结果汇总:")
|
||||
|
||||
passed = 0
|
||||
total = len(results)
|
||||
|
||||
for test_name, result in results:
|
||||
status = "✅ 通过" if result else "❌ 失败"
|
||||
print(f" {test_name}: {status}")
|
||||
if result:
|
||||
passed += 1
|
||||
|
||||
print(f"\n总体结果: {passed}/{total} 项测试通过")
|
||||
|
||||
if passed == total:
|
||||
print("🎉 Dummy2 机械臂已成功接入 UniLab 系统!")
|
||||
print("\n📋 后续步骤:")
|
||||
print("1. 启动 CAN2ETH 服务: ros2 launch dummy2_can2eth dummy2_can2eth_server.launch.py")
|
||||
print("2. 在 UniLab 界面中添加 Dummy2 设备实例")
|
||||
print("3. 测试设备初始化和基本功能")
|
||||
else:
|
||||
print("⚠️ 还有一些问题需要解决才能完全接入")
|
||||
|
||||
return passed == total
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -34,7 +34,7 @@ dependencies:
|
||||
- uvicorn
|
||||
- gradio
|
||||
- flask
|
||||
- websocket
|
||||
- websockets
|
||||
# Notebook
|
||||
- ipython
|
||||
- jupyter
|
||||
@@ -63,6 +63,9 @@ dependencies:
|
||||
# ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo
|
||||
# ilab equipments
|
||||
- uni-lab::ros-humble-unilabos-msgs
|
||||
- zeep
|
||||
- jinja2
|
||||
- pprp
|
||||
- pip:
|
||||
- paho-mqtt
|
||||
- opentrons_shared_data
|
||||
|
||||
@@ -34,7 +34,7 @@ dependencies:
|
||||
- uvicorn
|
||||
- gradio
|
||||
- flask
|
||||
- websocket
|
||||
- websockets
|
||||
# Notebook
|
||||
- ipython
|
||||
- jupyter
|
||||
@@ -62,6 +62,9 @@ dependencies:
|
||||
# ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo
|
||||
# ilab equipments
|
||||
- uni-lab::ros-humble-unilabos-msgs
|
||||
- zeep
|
||||
- jinja2
|
||||
- pprp
|
||||
- pip:
|
||||
- paho-mqtt
|
||||
- opentrons_shared_data
|
||||
|
||||
@@ -35,8 +35,7 @@ dependencies:
|
||||
- uvicorn
|
||||
- gradio
|
||||
- flask
|
||||
- websocket
|
||||
- paho-mqtt
|
||||
- websockets
|
||||
# Notebook
|
||||
- ipython
|
||||
- jupyter
|
||||
@@ -65,6 +64,9 @@ dependencies:
|
||||
# ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo
|
||||
# ilab equipments
|
||||
- uni-lab::ros-humble-unilabos-msgs
|
||||
- zeep
|
||||
- jinja2
|
||||
- pprp
|
||||
- pip:
|
||||
- paho-mqtt
|
||||
- opentrons_shared_data
|
||||
|
||||
@@ -34,7 +34,7 @@ dependencies:
|
||||
- uvicorn
|
||||
- gradio
|
||||
- flask
|
||||
- websocket
|
||||
- websockets
|
||||
# Notebook
|
||||
- ipython
|
||||
- jupyter
|
||||
@@ -65,6 +65,9 @@ dependencies:
|
||||
- uni-lab::ros-humble-unilabos-msgs
|
||||
# driver
|
||||
#- crcmod
|
||||
- zeep
|
||||
- jinja2
|
||||
- pprp
|
||||
- pip:
|
||||
- paho-mqtt
|
||||
- opentrons_shared_data
|
||||
|
||||
@@ -15,24 +15,33 @@ def start_backend(
|
||||
without_host: bool = False,
|
||||
visual: str = "None",
|
||||
resources_mesh_config: dict = {},
|
||||
**kwargs
|
||||
**kwargs,
|
||||
):
|
||||
if backend == "ros":
|
||||
# 假设 ros_main, simple_main, automancer_main 是不同 backend 的启动函数
|
||||
from unilabos.ros.main_slave_run import main, slave # 如果选择 'ros' 作为 backend
|
||||
elif backend == 'simple':
|
||||
elif backend == "simple":
|
||||
# 这里假设 simple_backend 和 automancer_backend 是你定义的其他两个后端
|
||||
# from simple_backend import main as simple_main
|
||||
pass
|
||||
elif backend == 'automancer':
|
||||
elif backend == "automancer":
|
||||
# from automancer_backend import main as automancer_main
|
||||
pass
|
||||
else:
|
||||
raise ValueError(f"Unsupported backend: {backend}")
|
||||
|
||||
|
||||
backend_thread = threading.Thread(
|
||||
target=main if not without_host else slave,
|
||||
args=(devices_config, resources_config, resources_edge_config, graph, controllers_config, bridges, visual, resources_mesh_config),
|
||||
args=(
|
||||
devices_config,
|
||||
resources_config,
|
||||
resources_edge_config,
|
||||
graph,
|
||||
controllers_config,
|
||||
bridges,
|
||||
visual,
|
||||
resources_mesh_config,
|
||||
),
|
||||
name="backend_thread",
|
||||
daemon=True,
|
||||
)
|
||||
|
||||
192
unilabos/app/communication.py
Normal file
192
unilabos/app/communication.py
Normal file
@@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env python
|
||||
# coding=utf-8
|
||||
"""
|
||||
通信模块
|
||||
|
||||
提供WebSocket的统一接口,支持通过配置选择通信协议。
|
||||
包含通信抽象层基类和通信客户端工厂。
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional
|
||||
from unilabos.config.config import BasicConfig
|
||||
from unilabos.utils import logger
|
||||
|
||||
|
||||
class BaseCommunicationClient(ABC):
|
||||
"""
|
||||
通信客户端抽象基类
|
||||
|
||||
定义了所有通信客户端(WebSocket等)需要实现的接口。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.is_disabled = True
|
||||
self.client_id = ""
|
||||
|
||||
@abstractmethod
|
||||
def start(self) -> None:
|
||||
"""
|
||||
启动通信客户端连接
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def stop(self) -> None:
|
||||
"""
|
||||
停止通信客户端连接
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def publish_device_status(self, device_status: dict, device_id: str, property_name: str) -> None:
|
||||
"""
|
||||
发布设备状态信息
|
||||
|
||||
Args:
|
||||
device_status: 设备状态字典
|
||||
device_id: 设备ID
|
||||
property_name: 属性名称
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def publish_job_status(
|
||||
self, feedback_data: dict, job_id: str, status: str, return_info: Optional[dict] = None
|
||||
) -> None:
|
||||
"""
|
||||
发布作业状态信息
|
||||
|
||||
Args:
|
||||
feedback_data: 反馈数据
|
||||
job_id: 作业ID
|
||||
status: 作业状态
|
||||
return_info: 返回信息
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def send_ping(self, ping_id: str, timestamp: float) -> None:
|
||||
"""
|
||||
发送ping消息
|
||||
|
||||
Args:
|
||||
ping_id: ping ID
|
||||
timestamp: 时间戳
|
||||
"""
|
||||
pass
|
||||
|
||||
def setup_pong_subscription(self) -> None:
|
||||
"""
|
||||
设置pong消息订阅(可选实现)
|
||||
"""
|
||||
pass
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
"""
|
||||
检查是否已连接
|
||||
|
||||
Returns:
|
||||
是否已连接
|
||||
"""
|
||||
return not self.is_disabled
|
||||
|
||||
|
||||
class CommunicationClientFactory:
|
||||
"""
|
||||
通信客户端工厂类
|
||||
|
||||
根据配置文件中的通信协议设置创建相应的客户端实例。
|
||||
"""
|
||||
|
||||
_client_cache: Optional[BaseCommunicationClient] = None
|
||||
|
||||
@classmethod
|
||||
def create_client(cls, protocol: Optional[str] = None) -> BaseCommunicationClient:
|
||||
"""
|
||||
创建通信客户端实例
|
||||
|
||||
Args:
|
||||
protocol: 指定的协议类型,如果为None则使用配置文件中的设置
|
||||
|
||||
Returns:
|
||||
通信客户端实例
|
||||
|
||||
Raises:
|
||||
ValueError: 当协议类型不支持时
|
||||
"""
|
||||
if protocol is None:
|
||||
protocol = BasicConfig.communication_protocol
|
||||
|
||||
protocol = protocol.lower()
|
||||
|
||||
if protocol == "websocket":
|
||||
return cls._create_websocket_client()
|
||||
else:
|
||||
logger.error(f"[CommunicationFactory] Unsupported protocol: {protocol}")
|
||||
logger.warning(f"[CommunicationFactory] Falling back to WebSocket")
|
||||
return cls._create_websocket_client()
|
||||
|
||||
@classmethod
|
||||
def get_client(cls, protocol: Optional[str] = None) -> BaseCommunicationClient:
|
||||
"""
|
||||
获取通信客户端实例(单例模式)
|
||||
|
||||
Args:
|
||||
protocol: 指定的协议类型,如果为None则使用配置文件中的设置
|
||||
|
||||
Returns:
|
||||
通信客户端实例
|
||||
"""
|
||||
if cls._client_cache is None:
|
||||
cls._client_cache = cls.create_client(protocol)
|
||||
logger.info(f"[CommunicationFactory] Created {type(cls._client_cache).__name__} client")
|
||||
|
||||
return cls._client_cache
|
||||
|
||||
@classmethod
|
||||
def _create_websocket_client(cls) -> BaseCommunicationClient:
|
||||
"""创建WebSocket客户端"""
|
||||
try:
|
||||
from unilabos.app.ws_client import WebSocketClient
|
||||
|
||||
return WebSocketClient()
|
||||
except Exception as e:
|
||||
logger.error(f"[CommunicationFactory] Failed to create WebSocket client: {str(e)}")
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
def reset_client(cls):
|
||||
"""重置客户端缓存(用于测试或重新配置)"""
|
||||
if cls._client_cache:
|
||||
try:
|
||||
cls._client_cache.stop()
|
||||
except Exception as e:
|
||||
logger.warning(f"[CommunicationFactory] Error stopping old client: {str(e)}")
|
||||
|
||||
cls._client_cache = None
|
||||
logger.info("[CommunicationFactory] Client cache reset")
|
||||
|
||||
@classmethod
|
||||
def get_supported_protocols(cls) -> list[str]:
|
||||
"""
|
||||
获取支持的协议列表
|
||||
|
||||
Returns:
|
||||
支持的协议列表
|
||||
"""
|
||||
return ["websocket"]
|
||||
|
||||
|
||||
def get_communication_client(protocol: Optional[str] = None) -> BaseCommunicationClient:
|
||||
"""
|
||||
获取通信客户端实例的便捷函数
|
||||
|
||||
Args:
|
||||
protocol: 指定的协议类型,如果为None则使用配置文件中的设置
|
||||
|
||||
Returns:
|
||||
通信客户端实例
|
||||
"""
|
||||
return CommunicationClientFactory.get_client(protocol)
|
||||
@@ -10,7 +10,6 @@ from copy import deepcopy
|
||||
|
||||
import yaml
|
||||
|
||||
from unilabos.resources.graphio import modify_to_backend_format
|
||||
|
||||
# 首先添加项目根目录到路径
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
@@ -18,11 +17,12 @@ unilabos_dir = os.path.dirname(os.path.dirname(current_dir))
|
||||
if unilabos_dir not in sys.path:
|
||||
sys.path.append(unilabos_dir)
|
||||
|
||||
from unilabos.config.config import load_config, BasicConfig
|
||||
from unilabos.config.config import load_config, BasicConfig, HTTPConfig
|
||||
from unilabos.utils.banner_print import print_status, print_unilab_banner
|
||||
from unilabos.resources.graphio import modify_to_backend_format
|
||||
|
||||
|
||||
def load_config_from_file(config_path, override_labid=None):
|
||||
def load_config_from_file(config_path):
|
||||
if config_path is None:
|
||||
config_path = os.environ.get("UNILABOS_BASICCONFIG_CONFIG_PATH", None)
|
||||
if config_path:
|
||||
@@ -31,10 +31,10 @@ def load_config_from_file(config_path, override_labid=None):
|
||||
elif not config_path.endswith(".py"):
|
||||
print_status(f"配置文件 {config_path} 不是Python文件,必须以.py结尾", "error")
|
||||
else:
|
||||
load_config(config_path, override_labid)
|
||||
load_config(config_path)
|
||||
else:
|
||||
print_status(f"启动 UniLab-OS时,配置文件参数未正确传入 --config '{config_path}' 尝试本地配置...", "warning")
|
||||
load_config(config_path, override_labid)
|
||||
load_config(config_path)
|
||||
|
||||
|
||||
def convert_argv_dashes_to_underscores(args: argparse.ArgumentParser):
|
||||
@@ -51,16 +51,14 @@ def convert_argv_dashes_to_underscores(args: argparse.ArgumentParser):
|
||||
def parse_args():
|
||||
"""解析命令行参数"""
|
||||
parser = argparse.ArgumentParser(description="Start Uni-Lab Edge server.")
|
||||
parser.add_argument("-g", "--graph", help="Physical setup graph.")
|
||||
# parser.add_argument("-d", "--devices", help="Devices config file.")
|
||||
# parser.add_argument("-r", "--resources", help="Resources config file.")
|
||||
parser.add_argument("-c", "--controllers", default=None, help="Controllers config file.")
|
||||
parser.add_argument("-g", "--graph", help="Physical setup graph file path.")
|
||||
parser.add_argument("-c", "--controllers", default=None, help="Controllers config file path.")
|
||||
parser.add_argument(
|
||||
"--registry_path",
|
||||
type=str,
|
||||
default=None,
|
||||
action="append",
|
||||
help="Path to the registry",
|
||||
help="Path to the registry directory",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--working_dir",
|
||||
@@ -77,67 +75,85 @@ def parse_args():
|
||||
parser.add_argument(
|
||||
"--app_bridges",
|
||||
nargs="+",
|
||||
default=["mqtt", "fastapi"],
|
||||
help="Bridges to connect to. Now support 'mqtt' and 'fastapi'.",
|
||||
default=["websocket", "fastapi"],
|
||||
help="Bridges to connect to. Now support 'websocket' and 'fastapi'.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--without_host",
|
||||
"--is_slave",
|
||||
action="store_true",
|
||||
help="Run the backend as slave (without host).",
|
||||
help="Run the backend as slave node (without host privileges).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--slave_no_host",
|
||||
action="store_true",
|
||||
help="Slave模式下跳过等待host服务",
|
||||
help="Skip waiting for host service in slave mode",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--upload_registry",
|
||||
action="store_true",
|
||||
help="启动unilab时同时报送注册表信息",
|
||||
help="Upload registry information when starting unilab",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--use_remote_resource",
|
||||
action="store_true",
|
||||
help="启动unilab时使用远程资源启动",
|
||||
help="Use remote resources when starting unilab",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
type=str,
|
||||
default=None,
|
||||
help="配置文件路径,支持.py格式的Python配置文件",
|
||||
help="Configuration file path, supports .py format Python config files",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=8002,
|
||||
help="信息页web服务的启动端口",
|
||||
help="Port for web service information page",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--disable_browser",
|
||||
action="store_true",
|
||||
help="是否在启动时关闭信息页",
|
||||
help="Disable opening information page on startup",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--2d_vis",
|
||||
action="store_true",
|
||||
help="是否在pylabrobot实例启动时,同时启动可视化",
|
||||
help="Enable 2D visualization when starting pylabrobot instance",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--visual",
|
||||
choices=["rviz", "web", "disable"],
|
||||
default="disable",
|
||||
help="选择可视化工具: rviz, web",
|
||||
help="Choose visualization tool: rviz, web, or disable",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--labid",
|
||||
"--ak",
|
||||
type=str,
|
||||
default="",
|
||||
help="实验室唯一ID,也可通过环境变量 UNILABOS_MQCONFIG_LABID 设置或传入--config设置",
|
||||
help="Access key for laboratory requests",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sk",
|
||||
type=str,
|
||||
default="",
|
||||
help="Secret key for laboratory requests",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--addr",
|
||||
type=str,
|
||||
default="https://uni-lab.bohrium.com/api/v1",
|
||||
help="Laboratory backend address",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip_env_check",
|
||||
action="store_true",
|
||||
help="跳过启动时的环境依赖检查",
|
||||
help="Skip environment dependency check on startup",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--complete_registry",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Complete registry information",
|
||||
)
|
||||
return parser
|
||||
|
||||
@@ -167,7 +183,7 @@ def main():
|
||||
else:
|
||||
working_dir = os.path.abspath(os.path.join(os.getcwd(), "unilabos_data"))
|
||||
if args_dict.get("working_dir"):
|
||||
working_dir = args_dict.get("working_dir")
|
||||
working_dir = args_dict.get("working_dir", "")
|
||||
if config_path and not os.path.exists(config_path):
|
||||
config_path = os.path.join(working_dir, "local_config.py")
|
||||
if not os.path.exists(config_path):
|
||||
@@ -192,17 +208,27 @@ def main():
|
||||
os.path.join(os.path.dirname(os.path.dirname(__file__)), "config", "example_config.py"), config_path
|
||||
)
|
||||
print_status(f"已创建 local_config.py 路径: {config_path}", "info")
|
||||
print_status(f"请在文件夹中配置lab_id,放入下载的CA.crt、lab.crt、lab.key重新启动本程序", "info")
|
||||
os._exit(1)
|
||||
else:
|
||||
os._exit(1)
|
||||
# 加载配置文件
|
||||
print_status(f"当前工作目录为 {working_dir}", "info")
|
||||
load_config_from_file(config_path, args_dict["labid"])
|
||||
load_config_from_file(config_path)
|
||||
if args_dict["addr"] == "test":
|
||||
print_status("使用测试环境地址", "info")
|
||||
HTTPConfig.remote_addr = "https://uni-lab.test.bohrium.com/api/v1"
|
||||
elif args_dict["addr"] == "uat":
|
||||
print_status("使用uat环境地址", "info")
|
||||
HTTPConfig.remote_addr = "https://uni-lab.uat.bohrium.com/api/v1"
|
||||
elif args_dict["addr"] == "local":
|
||||
print_status("使用本地环境地址", "info")
|
||||
HTTPConfig.remote_addr = "http://127.0.0.1:48197/api/v1"
|
||||
else:
|
||||
HTTPConfig.remote_addr = args_dict.get("addr", "")
|
||||
|
||||
if args_dict["use_remote_resource"]:
|
||||
print_status("使用远程资源启动", "info")
|
||||
from unilabos.app.web import http_client
|
||||
|
||||
res = http_client.resource_get("host_node", False)
|
||||
if str(res.get("code", 0)) == "0" and len(res.get("data", [])) > 0:
|
||||
print_status("远程资源已存在,使用云端物料!", "info")
|
||||
@@ -211,10 +237,17 @@ def main():
|
||||
print_status("远程资源不存在,本地将进行首次上报!", "info")
|
||||
|
||||
# 设置BasicConfig参数
|
||||
if args_dict.get("ak", ""):
|
||||
BasicConfig.ak = args_dict.get("ak", "")
|
||||
print_status("传入了ak参数,优先采用传入参数!", "info")
|
||||
if args_dict.get("sk", ""):
|
||||
BasicConfig.sk = args_dict.get("sk", "")
|
||||
print_status("传入了sk参数,优先采用传入参数!", "info")
|
||||
BasicConfig.working_dir = working_dir
|
||||
BasicConfig.is_host_mode = not args_dict.get("without_host", False)
|
||||
BasicConfig.is_host_mode = not args_dict.get("is_slave", False)
|
||||
BasicConfig.slave_no_host = args_dict.get("slave_no_host", False)
|
||||
BasicConfig.upload_registry = args_dict.get("upload_registry", False)
|
||||
BasicConfig.communication_protocol = "websocket"
|
||||
machine_name = os.popen("hostname").read().strip()
|
||||
machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name])
|
||||
BasicConfig.machine_name = machine_name
|
||||
@@ -227,17 +260,24 @@ def main():
|
||||
dict_to_nested_dict,
|
||||
initialize_resources,
|
||||
)
|
||||
from unilabos.app.mq import mqtt_client
|
||||
from unilabos.app.communication import get_communication_client
|
||||
from unilabos.registry.registry import build_registry
|
||||
from unilabos.app.backend import start_backend
|
||||
from unilabos.app.web import http_client
|
||||
from unilabos.app.web import start_server
|
||||
from unilabos.app.register import register_devices_and_resources
|
||||
|
||||
# 显示启动横幅
|
||||
print_unilab_banner(args_dict)
|
||||
|
||||
# 注册表
|
||||
build_registry(args_dict["registry_path"], False, args_dict["upload_registry"])
|
||||
lab_registry = build_registry(
|
||||
args_dict["registry_path"], args_dict.get("complete_registry", False), args_dict["upload_registry"]
|
||||
)
|
||||
|
||||
if not BasicConfig.ak or not BasicConfig.sk:
|
||||
print_status("后续运行必须拥有一个实验室,请前往 https://uni-lab.bohrium.com 注册实验室!", "warning")
|
||||
os._exit(1)
|
||||
if args_dict["graph"] is None:
|
||||
request_startup_json = http_client.request_startup_json()
|
||||
if not request_startup_json:
|
||||
@@ -258,6 +298,37 @@ def main():
|
||||
|
||||
graph_res.physical_setup_graph = graph
|
||||
resource_edge_info = modify_to_backend_format(data["links"])
|
||||
materials = lab_registry.obtain_registry_resource_info()
|
||||
materials.extend(lab_registry.obtain_registry_device_info())
|
||||
materials = {k["id"]: k for k in materials}
|
||||
nodes = {k["id"]: k for k in data["nodes"]}
|
||||
edge_info = len(resource_edge_info)
|
||||
for ind, i in enumerate(resource_edge_info[::-1]):
|
||||
source_node = nodes[i["source"]]
|
||||
target_node = nodes[i["target"]]
|
||||
source_handle = i["sourceHandle"]
|
||||
target_handle = i["targetHandle"]
|
||||
source_handler_keys = [
|
||||
h["handler_key"] for h in materials[source_node["class"]]["handles"] if h["io_type"] == "source"
|
||||
]
|
||||
target_handler_keys = [
|
||||
h["handler_key"] for h in materials[target_node["class"]]["handles"] if h["io_type"] == "target"
|
||||
]
|
||||
if source_handle not in source_handler_keys:
|
||||
print_status(
|
||||
f"节点 {source_node['id']} 的source端点 {source_handle} 不存在,请检查,支持的端点 {source_handler_keys}",
|
||||
"error",
|
||||
)
|
||||
resource_edge_info.pop(edge_info - ind - 1)
|
||||
continue
|
||||
if target_handle not in target_handler_keys:
|
||||
print_status(
|
||||
f"节点 {target_node['id']} 的target端点 {target_handle} 不存在,请检查,支持的端点 {target_handler_keys}",
|
||||
"error",
|
||||
)
|
||||
resource_edge_info.pop(edge_info - ind - 1)
|
||||
continue
|
||||
|
||||
devices_and_resources = dict_from_graph(graph_res.physical_setup_graph)
|
||||
# args_dict["resources_config"] = initialize_resources(list(deepcopy(devices_and_resources).values()))
|
||||
args_dict["resources_config"] = list(devices_and_resources.values())
|
||||
@@ -268,6 +339,22 @@ def main():
|
||||
for i in args_dict["resources_config"]:
|
||||
print_status(f"DeviceId: {i['id']}, Class: {i['class']}", "info")
|
||||
|
||||
if BasicConfig.upload_registry:
|
||||
# 设备注册到服务端 - 需要 ak 和 sk
|
||||
if args_dict.get("ak") and args_dict.get("sk"):
|
||||
print_status("开始注册设备到服务端...", "info")
|
||||
try:
|
||||
register_devices_and_resources(lab_registry)
|
||||
print_status("设备注册完成", "info")
|
||||
except Exception as e:
|
||||
print_status(f"设备注册失败: {e}", "error")
|
||||
else:
|
||||
print_status("未提供 ak 和 sk,跳过设备注册", "info")
|
||||
else:
|
||||
print_status(
|
||||
"本次启动注册表不报送云端,如果您需要联网调试,请在启动命令增加--upload_registry", "warning"
|
||||
)
|
||||
|
||||
if args_dict["controllers"] is not None:
|
||||
args_dict["controllers_config"] = yaml.safe_load(open(args_dict["controllers"], encoding="utf-8"))
|
||||
else:
|
||||
@@ -275,19 +362,22 @@ def main():
|
||||
|
||||
args_dict["bridges"] = []
|
||||
|
||||
if "mqtt" in args_dict["app_bridges"]:
|
||||
args_dict["bridges"].append(mqtt_client)
|
||||
# 获取通信客户端(仅支持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 "mqtt" in args_dict["app_bridges"]:
|
||||
if "websocket" in args_dict["app_bridges"]:
|
||||
|
||||
def _exit(signum, frame):
|
||||
mqtt_client.stop()
|
||||
comm_client.stop()
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, _exit)
|
||||
signal.signal(signal.SIGTERM, _exit)
|
||||
mqtt_client.start()
|
||||
comm_client.start()
|
||||
args_dict["resources_mesh_config"] = {}
|
||||
args_dict["resources_edge_config"] = resource_edge_info
|
||||
# web visiualize 2D
|
||||
|
||||
@@ -50,11 +50,16 @@ class Resp(BaseModel):
|
||||
|
||||
class JobAddReq(BaseModel):
|
||||
device_id: str = Field(examples=["Gripper"], description="device id")
|
||||
data: dict = Field(examples=[{"position": 30, "torque": 5, "action": "push_to"}])
|
||||
action: str = Field(examples=["_execute_driver_command_async"], description="action name", default="")
|
||||
action_type: str = Field(examples=["unilabos_msgs.action._str_single_input.StrSingleInput"], description="action name", default="")
|
||||
action_args: dict = Field(examples=[{'string': 'string'}], description="action name", default="")
|
||||
task_id: str = Field(examples=["task_id"], description="task uuid")
|
||||
job_id: str = Field(examples=["job_id"], description="goal uuid")
|
||||
node_id: str = Field(examples=["node_id"], description="node uuid")
|
||||
server_info: dict = Field(examples=[{"send_timestamp": 1717000000.0}], description="server info")
|
||||
|
||||
data: dict = Field(examples=[{"position": 30, "torque": 5, "action": "push_to"}], default={})
|
||||
|
||||
|
||||
class JobStepFinishReq(BaseModel):
|
||||
token: str = Field(examples=["030944"], description="token")
|
||||
|
||||
@@ -1,221 +0,0 @@
|
||||
import json
|
||||
import time
|
||||
import traceback
|
||||
from typing import Optional
|
||||
import uuid
|
||||
|
||||
import paho.mqtt.client as mqtt
|
||||
import ssl
|
||||
import base64
|
||||
import hmac
|
||||
from hashlib import sha1
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
from unilabos.config.config import MQConfig
|
||||
from unilabos.app.controler import job_add
|
||||
from unilabos.app.model import JobAddReq
|
||||
from unilabos.utils import logger
|
||||
from unilabos.utils.type_check import TypeEncoder
|
||||
|
||||
from paho.mqtt.enums import CallbackAPIVersion
|
||||
|
||||
|
||||
class MQTTClient:
|
||||
mqtt_disable = True
|
||||
|
||||
def __init__(self):
|
||||
self.mqtt_disable = not MQConfig.lab_id
|
||||
self.client_id = f"{MQConfig.group_id}@@@{MQConfig.lab_id}{uuid.uuid4()}"
|
||||
logger.info("[MQTT] Client_id: " + self.client_id)
|
||||
self.client = mqtt.Client(CallbackAPIVersion.VERSION2, client_id=self.client_id, protocol=mqtt.MQTTv5)
|
||||
self._setup_callbacks()
|
||||
|
||||
def _setup_callbacks(self):
|
||||
self.client.on_log = self._on_log
|
||||
self.client.on_connect = self._on_connect
|
||||
self.client.on_message = self._on_message
|
||||
self.client.on_disconnect = self._on_disconnect
|
||||
|
||||
def _on_log(self, client, userdata, level, buf):
|
||||
# logger.info(f"[MQTT] log: {buf}")
|
||||
pass
|
||||
|
||||
def _on_connect(self, client, userdata, flags, rc, properties=None):
|
||||
logger.info("[MQTT] Connected with result code " + str(rc))
|
||||
client.subscribe(f"labs/{MQConfig.lab_id}/job/start/", 0)
|
||||
client.subscribe(f"labs/{MQConfig.lab_id}/pong/", 0)
|
||||
|
||||
def _on_message(self, client, userdata, msg) -> None:
|
||||
# logger.info("[MQTT] on_message<<<< " + msg.topic + " " + str(msg.payload))
|
||||
try:
|
||||
payload_str = msg.payload.decode("utf-8")
|
||||
payload_json = json.loads(payload_str)
|
||||
if msg.topic == f"labs/{MQConfig.lab_id}/job/start/":
|
||||
if "data" not in payload_json:
|
||||
payload_json["data"] = {}
|
||||
if "action" in payload_json:
|
||||
payload_json["data"]["action"] = payload_json.pop("action")
|
||||
if "action_type" in payload_json:
|
||||
payload_json["data"]["action_type"] = payload_json.pop("action_type")
|
||||
if "action_args" in payload_json:
|
||||
payload_json["data"]["action_args"] = payload_json.pop("action_args")
|
||||
if "action_kwargs" in payload_json:
|
||||
payload_json["data"]["action_kwargs"] = payload_json.pop("action_kwargs")
|
||||
job_req = JobAddReq.model_validate(payload_json)
|
||||
data = job_add(job_req)
|
||||
return
|
||||
elif msg.topic == f"labs/{MQConfig.lab_id}/pong/":
|
||||
# 处理pong响应,通知HostNode
|
||||
from unilabos.ros.nodes.presets.host_node import HostNode
|
||||
|
||||
host_instance = HostNode.get_instance(0)
|
||||
if host_instance:
|
||||
host_instance.handle_pong_response(payload_json)
|
||||
return
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"[MQTT] JSON 解析错误: {e}")
|
||||
logger.error(f"[MQTT] Raw message: {msg.payload}")
|
||||
logger.error(traceback.format_exc())
|
||||
except Exception as e:
|
||||
logger.error(f"[MQTT] 处理消息时出错: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
def _on_disconnect(self, client, userdata, rc, reasonCode=None, properties=None):
|
||||
if rc != 0:
|
||||
logger.error(f"[MQTT] Unexpected disconnection {rc}")
|
||||
|
||||
def _setup_ssl_context(self):
|
||||
temp_files = []
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(mode="w", delete=False) as ca_temp:
|
||||
ca_temp.write(MQConfig.ca_content)
|
||||
temp_files.append(ca_temp.name)
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="w", delete=False) as cert_temp:
|
||||
cert_temp.write(MQConfig.cert_content)
|
||||
temp_files.append(cert_temp.name)
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="w", delete=False) as key_temp:
|
||||
key_temp.write(MQConfig.key_content)
|
||||
temp_files.append(key_temp.name)
|
||||
|
||||
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
|
||||
context.load_verify_locations(cafile=temp_files[0])
|
||||
context.load_cert_chain(certfile=temp_files[1], keyfile=temp_files[2])
|
||||
self.client.tls_set_context(context)
|
||||
finally:
|
||||
for temp_file in temp_files:
|
||||
try:
|
||||
os.unlink(temp_file)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
def start(self):
|
||||
if self.mqtt_disable:
|
||||
logger.warning("MQTT is disabled, skipping connection.")
|
||||
return
|
||||
userName = f"Signature|{MQConfig.access_key}|{MQConfig.instance_id}"
|
||||
password = base64.b64encode(
|
||||
hmac.new(MQConfig.secret_key.encode(), self.client_id.encode(), sha1).digest()
|
||||
).decode()
|
||||
|
||||
self.client.username_pw_set(userName, password)
|
||||
self._setup_ssl_context()
|
||||
|
||||
# 创建连接线程
|
||||
def connect_thread_func():
|
||||
try:
|
||||
self.client.connect(MQConfig.broker_url, MQConfig.port, 60)
|
||||
self.client.loop_start()
|
||||
|
||||
# 添加连接超时检测
|
||||
max_attempts = 5
|
||||
attempt = 0
|
||||
while not self.client.is_connected() and attempt < max_attempts:
|
||||
logger.info(
|
||||
f"[MQTT] 正在连接到 {MQConfig.broker_url}:{MQConfig.port},尝试 {attempt+1}/{max_attempts}"
|
||||
)
|
||||
time.sleep(3)
|
||||
attempt += 1
|
||||
|
||||
if self.client.is_connected():
|
||||
logger.info(f"[MQTT] 已成功连接到 {MQConfig.broker_url}:{MQConfig.port}")
|
||||
else:
|
||||
logger.error(f"[MQTT] 连接超时,可能是账号密码错误或网络问题")
|
||||
self.client.loop_stop()
|
||||
except Exception as e:
|
||||
logger.error(f"[MQTT] 连接失败: {str(e)}")
|
||||
|
||||
connect_thread_func()
|
||||
# connect_thread = threading.Thread(target=connect_thread_func)
|
||||
# connect_thread.daemon = True
|
||||
# connect_thread.start()
|
||||
|
||||
def stop(self):
|
||||
if self.mqtt_disable:
|
||||
return
|
||||
self.client.disconnect()
|
||||
self.client.loop_stop()
|
||||
|
||||
def publish_device_status(self, device_status: dict, device_id, property_name):
|
||||
# status = device_status.get(device_id, {})
|
||||
if self.mqtt_disable:
|
||||
return
|
||||
status = {"data": device_status.get(device_id, {}), "device_id": device_id, "timestamp": time.time()}
|
||||
address = f"labs/{MQConfig.lab_id}/devices/"
|
||||
self.client.publish(address, json.dumps(status), qos=2)
|
||||
# logger.info(f"Device {device_id} status published: address: {address}, {status}")
|
||||
|
||||
def publish_job_status(self, feedback_data: dict, job_id: str, status: str, return_info: Optional[str] = None):
|
||||
if self.mqtt_disable:
|
||||
return
|
||||
if return_info is None:
|
||||
return_info = "{}"
|
||||
jobdata = {"job_id": job_id, "data": feedback_data, "status": status, "return_info": return_info}
|
||||
self.client.publish(f"labs/{MQConfig.lab_id}/job/list/", json.dumps(jobdata), qos=2)
|
||||
|
||||
def publish_registry(self, device_id: str, device_info: dict, print_debug: bool = True):
|
||||
if self.mqtt_disable:
|
||||
return
|
||||
address = f"labs/{MQConfig.lab_id}/registry/"
|
||||
registry_data = json.dumps({device_id: device_info}, ensure_ascii=False, cls=TypeEncoder)
|
||||
self.client.publish(address, registry_data, qos=2)
|
||||
if print_debug:
|
||||
logger.debug(f"Registry data published: address: {address}, {registry_data}")
|
||||
|
||||
def publish_actions(self, action_id: str, action_info: dict):
|
||||
if self.mqtt_disable:
|
||||
return
|
||||
address = f"labs/{MQConfig.lab_id}/actions/"
|
||||
self.client.publish(address, json.dumps(action_info), qos=2)
|
||||
logger.debug(f"Action data published: address: {address}, {action_id}, {action_info}")
|
||||
|
||||
def send_ping(self, ping_id: str, timestamp: float):
|
||||
"""发送ping消息到服务端"""
|
||||
if self.mqtt_disable:
|
||||
return
|
||||
address = f"labs/{MQConfig.lab_id}/ping/"
|
||||
ping_data = {"ping_id": ping_id, "client_timestamp": timestamp, "type": "ping"}
|
||||
self.client.publish(address, json.dumps(ping_data), qos=2)
|
||||
|
||||
def setup_pong_subscription(self):
|
||||
"""设置pong消息订阅"""
|
||||
if self.mqtt_disable:
|
||||
return
|
||||
pong_topic = f"labs/{MQConfig.lab_id}/pong/"
|
||||
self.client.subscribe(pong_topic, 0)
|
||||
logger.debug(f"Subscribed to pong topic: {pong_topic}")
|
||||
|
||||
def handle_pong(self, pong_data: dict):
|
||||
"""处理pong响应(这个方法会在收到pong消息时被调用)"""
|
||||
logger.debug(f"Pong received: {pong_data}")
|
||||
# 这里会被HostNode的ping-pong处理逻辑调用
|
||||
pass
|
||||
|
||||
|
||||
mqtt_client = MQTTClient()
|
||||
|
||||
if __name__ == "__main__":
|
||||
mqtt_client.start()
|
||||
@@ -1,85 +1,62 @@
|
||||
import argparse
|
||||
import json
|
||||
import time
|
||||
|
||||
from unilabos.config.config import BasicConfig
|
||||
from unilabos.registry.registry import build_registry
|
||||
|
||||
from unilabos.app.main import load_config_from_file
|
||||
from unilabos.utils.log import logger
|
||||
from unilabos.utils.type_check import TypeEncoder
|
||||
|
||||
|
||||
def register_devices_and_resources(mqtt_client, lab_registry):
|
||||
def register_devices_and_resources(lab_registry):
|
||||
"""
|
||||
注册设备和资源到 MQTT
|
||||
注册设备和资源到服务器(仅支持HTTP)
|
||||
"""
|
||||
logger.info("[UniLab Register] 开始注册设备和资源...")
|
||||
|
||||
# 注册设备信息
|
||||
for device_info in lab_registry.obtain_registry_device_info():
|
||||
mqtt_client.publish_registry(device_info["id"], device_info, False)
|
||||
logger.debug(f"[UniLab Register] 注册设备: {device_info['id']}")
|
||||
|
||||
# # 注册资源信息
|
||||
# for resource_info in lab_registry.obtain_registry_resource_info():
|
||||
# mqtt_client.publish_registry(resource_info["id"], resource_info, False)
|
||||
# logger.debug(f"[UniLab Register] 注册资源: {resource_info['id']}")
|
||||
|
||||
# 注册资源信息 - 使用HTTP方式
|
||||
from unilabos.app.web.client import http_client
|
||||
|
||||
logger.info("[UniLab Register] 开始注册设备和资源...")
|
||||
|
||||
# 注册设备信息
|
||||
devices_to_register = {}
|
||||
for device_info in lab_registry.obtain_registry_device_info():
|
||||
devices_to_register[device_info["id"]] = json.loads(
|
||||
json.dumps(device_info, ensure_ascii=False, cls=TypeEncoder)
|
||||
)
|
||||
logger.debug(f"[UniLab Register] 收集设备: {device_info['id']}")
|
||||
|
||||
resources_to_register = {}
|
||||
for resource_info in lab_registry.obtain_registry_resource_info():
|
||||
resources_to_register[resource_info["id"]] = resource_info
|
||||
logger.debug(f"[UniLab Register] 准备注册资源: {resource_info['id']}")
|
||||
logger.debug(f"[UniLab Register] 收集资源: {resource_info['id']}")
|
||||
|
||||
# 注册设备
|
||||
if devices_to_register:
|
||||
try:
|
||||
start_time = time.time()
|
||||
response = http_client.resource_registry({"resources": list(devices_to_register.values())})
|
||||
cost_time = time.time() - start_time
|
||||
if response.status_code in [200, 201]:
|
||||
logger.info(f"[UniLab Register] 成功注册 {len(devices_to_register)} 个设备 {cost_time}ms")
|
||||
else:
|
||||
logger.error(f"[UniLab Register] 设备注册失败: {response.status_code}, {response.text} {cost_time}ms")
|
||||
except Exception as e:
|
||||
logger.error(f"[UniLab Register] 设备注册异常: {e}")
|
||||
|
||||
# 注册资源
|
||||
if resources_to_register:
|
||||
start_time = time.time()
|
||||
response = http_client.resource_registry(resources_to_register)
|
||||
cost_time = time.time() - start_time
|
||||
if response.status_code in [200, 201]:
|
||||
logger.info(f"[UniLab Register] 成功通过HTTP注册 {len(resources_to_register)} 个资源 {cost_time}ms")
|
||||
else:
|
||||
logger.error(f"[UniLab Register] HTTP注册资源失败: {response.status_code}, {response.text} {cost_time}ms")
|
||||
try:
|
||||
start_time = time.time()
|
||||
response = http_client.resource_registry({"resources": list(resources_to_register.values())})
|
||||
cost_time = time.time() - start_time
|
||||
if response.status_code in [200, 201]:
|
||||
logger.info(f"[UniLab Register] 成功注册 {len(resources_to_register)} 个资源 {cost_time}ms")
|
||||
else:
|
||||
logger.error(f"[UniLab Register] 资源注册失败: {response.status_code}, {response.text} {cost_time}ms")
|
||||
except Exception as e:
|
||||
logger.error(f"[UniLab Register] 资源注册异常: {e}")
|
||||
|
||||
logger.info("[UniLab Register] 设备和资源注册完成.")
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
命令行入口函数
|
||||
"""
|
||||
parser = argparse.ArgumentParser(description="注册设备和资源到 MQTT")
|
||||
parser.add_argument(
|
||||
"--registry",
|
||||
type=str,
|
||||
default=None,
|
||||
action="append",
|
||||
help="注册表路径",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
type=str,
|
||||
default=None,
|
||||
help="配置文件路径,支持.py格式的Python配置文件",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--complete_registry",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="是否补全注册表",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
load_config_from_file(args.config)
|
||||
# 构建注册表
|
||||
build_registry(args.registry, args.complete_registry, True)
|
||||
from unilabos.app.mq import mqtt_client
|
||||
|
||||
# 连接mqtt
|
||||
mqtt_client.start()
|
||||
|
||||
from unilabos.registry.registry import lab_registry
|
||||
|
||||
# 注册设备和资源
|
||||
register_devices_and_resources(mqtt_client, lab_registry)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,13 +3,14 @@ HTTP客户端模块
|
||||
|
||||
提供与远程服务器通信的客户端功能,只有host需要用
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
import requests
|
||||
from unilabos.utils.log import info
|
||||
from unilabos.config.config import MQConfig, HTTPConfig, BasicConfig
|
||||
from unilabos.config.config import HTTPConfig, BasicConfig
|
||||
from unilabos.utils import logger
|
||||
|
||||
|
||||
@@ -24,14 +25,17 @@ class HTTPClient:
|
||||
remote_addr: 远程服务器地址,如果不提供则从配置中获取
|
||||
auth: 授权信息
|
||||
"""
|
||||
self.initialized = False
|
||||
self.remote_addr = remote_addr or HTTPConfig.remote_addr
|
||||
if auth is not None:
|
||||
self.auth = auth
|
||||
else:
|
||||
self.auth = MQConfig.lab_id
|
||||
auth_secret = BasicConfig.auth_secret()
|
||||
self.auth = auth_secret
|
||||
info(f"正在使用ak sk作为授权信息:[{auth_secret}]")
|
||||
info(f"HTTPClient 初始化完成: remote_addr={self.remote_addr}")
|
||||
|
||||
def resource_edge_add(self, resources: List[Dict[str, Any]], database_process_later: bool) -> requests.Response:
|
||||
def resource_edge_add(self, resources: List[Dict[str, Any]]) -> requests.Response:
|
||||
"""
|
||||
添加资源
|
||||
|
||||
@@ -41,33 +45,51 @@ class HTTPClient:
|
||||
Returns:
|
||||
Response: API响应对象
|
||||
"""
|
||||
database_param = 1 if database_process_later else 0
|
||||
response = requests.post(
|
||||
f"{self.remote_addr}/lab/resource/edge/batch_create/?database_process_later={database_param}",
|
||||
json=resources,
|
||||
headers={"Authorization": f"lab {self.auth}"},
|
||||
f"{self.remote_addr}/lab/material/edge",
|
||||
json={
|
||||
"edges": resources,
|
||||
},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=100,
|
||||
)
|
||||
if response.status_code == 200:
|
||||
res = response.json()
|
||||
if "code" in res and res["code"] != 0:
|
||||
logger.error(f"添加物料关系失败: {response.text}")
|
||||
if response.status_code != 200 and response.status_code != 201:
|
||||
logger.error(f"添加物料关系失败: {response.status_code}, {response.text}")
|
||||
return response
|
||||
|
||||
def resource_add(self, resources: List[Dict[str, Any]], database_process_later: bool) -> requests.Response:
|
||||
def resource_add(self, resources: List[Dict[str, Any]]) -> requests.Response:
|
||||
"""
|
||||
添加资源
|
||||
|
||||
Args:
|
||||
resources: 要添加的资源列表
|
||||
database_process_later: 后台处理资源
|
||||
Returns:
|
||||
Response: API响应对象
|
||||
"""
|
||||
response = requests.post(
|
||||
f"{self.remote_addr}/lab/resource/?database_process_later={1 if database_process_later else 0}",
|
||||
json=resources,
|
||||
headers={"Authorization": f"lab {self.auth}"},
|
||||
timeout=100,
|
||||
)
|
||||
if not self.initialized:
|
||||
self.initialized = True
|
||||
info(f"首次添加资源,当前远程地址: {self.remote_addr}")
|
||||
response = requests.post(
|
||||
f"{self.remote_addr}/lab/material",
|
||||
json={"nodes": resources},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=100,
|
||||
)
|
||||
else:
|
||||
response = requests.put(
|
||||
f"{self.remote_addr}/lab/material",
|
||||
json={"nodes": resources},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=100,
|
||||
)
|
||||
if response.status_code == 200:
|
||||
res = response.json()
|
||||
if "code" in res and res["code"] != 0:
|
||||
logger.error(f"添加物料失败: {response.text}")
|
||||
if response.status_code != 200:
|
||||
logger.error(f"添加物料失败: {response.text}")
|
||||
return response
|
||||
@@ -84,9 +106,9 @@ class HTTPClient:
|
||||
Dict: 返回的资源数据
|
||||
"""
|
||||
response = requests.get(
|
||||
f"{self.remote_addr}/lab/resource/?edge_format=1",
|
||||
f"{self.remote_addr}/lab/material",
|
||||
params={"id": id, "with_children": with_children},
|
||||
headers={"Authorization": f"lab {self.auth}"},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=20,
|
||||
)
|
||||
return response.json()
|
||||
@@ -104,7 +126,7 @@ class HTTPClient:
|
||||
response = requests.delete(
|
||||
f"{self.remote_addr}/lab/resource/batch_delete/",
|
||||
params={"id": id},
|
||||
headers={"Authorization": f"lab {self.auth}"},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=20,
|
||||
)
|
||||
return response
|
||||
@@ -119,13 +141,29 @@ class HTTPClient:
|
||||
Returns:
|
||||
Response: API响应对象
|
||||
"""
|
||||
response = requests.patch(
|
||||
f"{self.remote_addr}/lab/resource/batch_update/?edge_format=1",
|
||||
json=resources,
|
||||
headers={"Authorization": f"lab {self.auth}"},
|
||||
timeout=100,
|
||||
)
|
||||
return response
|
||||
if not self.initialized:
|
||||
self.initialized = True
|
||||
info(f"首次添加资源,当前远程地址: {self.remote_addr}")
|
||||
response = requests.post(
|
||||
f"{self.remote_addr}/lab/material",
|
||||
json={"nodes": resources},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=100,
|
||||
)
|
||||
else:
|
||||
response = requests.put(
|
||||
f"{self.remote_addr}/lab/material",
|
||||
json={"nodes": resources},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=100,
|
||||
)
|
||||
if response.status_code == 200:
|
||||
res = response.json()
|
||||
if "code" in res and res["code"] != 0:
|
||||
logger.error(f"添加物料失败: {response.text}")
|
||||
if response.status_code != 200:
|
||||
logger.error(f"添加物料失败: {response.text}")
|
||||
return response.json()
|
||||
|
||||
def upload_file(self, file_path: str, scene: str = "models") -> requests.Response:
|
||||
"""
|
||||
@@ -146,25 +184,25 @@ class HTTPClient:
|
||||
response = requests.post(
|
||||
f"{self.remote_addr}/api/account/file_upload/{scene}",
|
||||
files=files,
|
||||
headers={"Authorization": f"lab {self.auth}"},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=30, # 上传文件可能需要更长的超时时间
|
||||
)
|
||||
return response
|
||||
|
||||
def resource_registry(self, registry_data: Dict[str, Any]) -> requests.Response:
|
||||
def resource_registry(self, registry_data: Dict[str, Any] | List[Dict[str, Any]]) -> requests.Response:
|
||||
"""
|
||||
注册资源到服务器
|
||||
|
||||
Args:
|
||||
registry_data: 注册表数据,格式为 {resource_id: resource_info}
|
||||
registry_data: 注册表数据,格式为 {resource_id: resource_info} / [{resource_info}]
|
||||
|
||||
Returns:
|
||||
Response: API响应对象
|
||||
"""
|
||||
response = requests.post(
|
||||
f"{self.remote_addr}/lab/registry/",
|
||||
f"{self.remote_addr}/lab/resource",
|
||||
json=registry_data,
|
||||
headers={"Authorization": f"lab {self.auth}"},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=30,
|
||||
)
|
||||
if response.status_code not in [200, 201]:
|
||||
@@ -183,7 +221,7 @@ class HTTPClient:
|
||||
"""
|
||||
response = requests.get(
|
||||
f"{self.remote_addr}/lab/resource/graph_info/",
|
||||
headers={"Authorization": f"lab {self.auth}"},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=(3, 30),
|
||||
)
|
||||
if response.status_code != 200:
|
||||
|
||||
@@ -78,21 +78,23 @@ def setup_web_pages(router: APIRouter) -> None:
|
||||
HTMLResponse: 渲染后的HTML页面
|
||||
"""
|
||||
try:
|
||||
# 准备设备数据
|
||||
# 准备初始数据结构(这些数据将通过WebSocket实时更新)
|
||||
devices = []
|
||||
resources = []
|
||||
modules = {"names": [], "classes": [], "displayed_count": 0, "total_count": 0}
|
||||
|
||||
# 获取在线设备信息
|
||||
# 获取在线设备信息(用于初始渲染)
|
||||
ros_node_info = get_ros_node_info()
|
||||
# 获取主机节点信息
|
||||
# 获取主机节点信息(用于初始渲染)
|
||||
host_node_info = get_host_node_info()
|
||||
# 获取Registry路径信息
|
||||
# 获取Registry路径信息(静态信息,不需要实时更新)
|
||||
registry_info = get_registry_info()
|
||||
|
||||
# 获取已加载的设备
|
||||
# 获取初始数据用于页面渲染(后续将被WebSocket数据覆盖)
|
||||
if lab_registry:
|
||||
devices = json.loads(json.dumps(lab_registry.obtain_registry_device_info(), ensure_ascii=False, cls=TypeEncoder))
|
||||
devices = json.loads(
|
||||
json.dumps(lab_registry.obtain_registry_device_info(), ensure_ascii=False, cls=TypeEncoder)
|
||||
)
|
||||
# 资源类型
|
||||
for resource_id, resource_info in lab_registry.resource_type_registry.items():
|
||||
resources.append(
|
||||
@@ -103,7 +105,7 @@ def setup_web_pages(router: APIRouter) -> None:
|
||||
}
|
||||
)
|
||||
|
||||
# 获取导入的模块
|
||||
# 获取导入的模块(初始数据)
|
||||
if msg_converter_manager:
|
||||
modules["names"] = msg_converter_manager.list_modules()
|
||||
all_classes = [i for i in msg_converter_manager.list_classes() if "." in i]
|
||||
@@ -171,3 +173,20 @@ def setup_web_pages(router: APIRouter) -> None:
|
||||
except Exception as e:
|
||||
error(f"打开文件夹时出错: {str(e)}")
|
||||
return {"status": "error", "message": f"Failed to open folder: {str(e)}"}
|
||||
|
||||
@router.get("/registry-editor", response_class=HTMLResponse, summary="Registry Editor")
|
||||
async def registry_editor_page() -> str:
|
||||
"""
|
||||
注册表编辑页面,用于导入Python文件并生成注册表
|
||||
|
||||
Returns:
|
||||
HTMLResponse: 渲染后的HTML页面
|
||||
"""
|
||||
try:
|
||||
# 使用模板渲染页面
|
||||
template = env.get_template("registry_editor.html")
|
||||
html = template.render()
|
||||
return html
|
||||
except Exception as e:
|
||||
error(f"生成注册表编辑页面时出错: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Error generating registry editor page: {str(e)}")
|
||||
|
||||
@@ -162,7 +162,6 @@
|
||||
<body>
|
||||
<h1>{% block header %}UniLab{% endblock %}</h1>
|
||||
{% block nav %}
|
||||
<a href="/unilabos/webtic" class="home-link">Home</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block top_info %}{% endblock %}
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}UniLab API{% endblock %}
|
||||
|
||||
{% block header %}UniLab API{% endblock %}
|
||||
|
||||
{% block nav %}
|
||||
<a href="/status" class="status-link">System Status</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<h2>Available Endpoints</h2>
|
||||
{% for route in routes %}
|
||||
<div class="endpoint">
|
||||
<span class="method">{{ route.method }}</span>
|
||||
<a href="{{ route.path }}">{{ route.path }}</a>
|
||||
<p>{{ route.summary }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% extends "base.html" %} {% block title %}UniLab API{% endblock %} {% block
|
||||
header %}UniLab API{% endblock %} {% block nav %}
|
||||
<div class="nav-tabs">
|
||||
<a
|
||||
href="/"
|
||||
class="nav-tab"
|
||||
style="background-color: #2196f3; color: white"
|
||||
target="_blank"
|
||||
>主页</a
|
||||
>
|
||||
<a href="/status" class="nav-tab">状态</a>
|
||||
<a href="/registry-editor" class="nav-tab" target="_blank">注册表编辑</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %} {% block content %}
|
||||
<div class="card">
|
||||
<h2>Available Endpoints</h2>
|
||||
{% for route in routes %}
|
||||
<div class="endpoint">
|
||||
<span class="method">{{ route.method }}</span>
|
||||
<a href="{{ route.path }}">{{ route.path }}</a>
|
||||
<p>{{ route.summary }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
1411
unilabos/app/web/templates/registry_editor.html
Normal file
1411
unilabos/app/web/templates/registry_editor.html
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1166
unilabos/app/ws_client.py
Normal file
1166
unilabos/app/ws_client.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -46,6 +46,7 @@ action_protocol_generators = {
|
||||
HeatChillStopProtocol: generate_heat_chill_stop_protocol,
|
||||
HydrogenateProtocol: generate_hydrogenate_protocol,
|
||||
PumpTransferProtocol: generate_pump_protocol_with_rinsing,
|
||||
TransferProtocol: generate_pump_protocol,
|
||||
RecrystallizeProtocol: generate_recrystallize_protocol,
|
||||
ResetHandlingProtocol: generate_reset_handling_protocol,
|
||||
RunColumnProtocol: generate_run_column_protocol,
|
||||
|
||||
@@ -155,7 +155,7 @@ def generate_add_protocol(
|
||||
"device_id": stirrer_id,
|
||||
"action_name": "start_stir",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel_id, # 🔧 使用 vessel_id
|
||||
"vessel": {"id": vessel_id}, # 🔧 使用 vessel_id
|
||||
"stir_speed": stir_speed,
|
||||
"purpose": f"准备添加固体 {reagent}"
|
||||
}
|
||||
@@ -169,7 +169,7 @@ def generate_add_protocol(
|
||||
|
||||
# 固体加样
|
||||
add_kwargs = {
|
||||
"vessel": vessel_id, # 🔧 使用 vessel_id
|
||||
"vessel": {"id": vessel_id}, # 🔧 使用 vessel_id
|
||||
"reagent": reagent,
|
||||
"purpose": purpose,
|
||||
"event": event,
|
||||
@@ -232,7 +232,7 @@ def generate_add_protocol(
|
||||
"device_id": stirrer_id,
|
||||
"action_name": "start_stir",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel_id, # 🔧 使用 vessel_id
|
||||
"vessel": {"id": vessel_id}, # 🔧 使用 vessel_id
|
||||
"stir_speed": stir_speed,
|
||||
"purpose": f"准备添加液体 {reagent}"
|
||||
}
|
||||
|
||||
@@ -325,7 +325,7 @@ def generate_adjust_ph_protocol(
|
||||
"device_id": stirrer_id,
|
||||
"action_name": "start_stir",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel_id,
|
||||
"vessel": {"id": vessel_id},
|
||||
"stir_speed": stir_speed,
|
||||
"purpose": f"pH调节: 启动搅拌,准备添加 {reagent}"
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ def generate_centrifuge_protocol(
|
||||
"device_id": centrifuge_id,
|
||||
"action_name": "centrifuge",
|
||||
"action_kwargs": {
|
||||
"vessel": centrifuge_vessel,
|
||||
"vessel": {"id": centrifuge_vessel},
|
||||
"speed": speed,
|
||||
"time": time,
|
||||
"temp": temp
|
||||
|
||||
@@ -143,7 +143,7 @@ def generate_clean_vessel_protocol(
|
||||
"device_id": heatchill_id,
|
||||
"action_name": "heat_chill_start",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel_id, # 🔧 使用 vessel_id
|
||||
"vessel": {"id": vessel_id}, # 🔧 使用 vessel_id
|
||||
"temp": temp,
|
||||
"purpose": f"cleaning with {solvent}"
|
||||
}
|
||||
@@ -295,7 +295,7 @@ def generate_clean_vessel_protocol(
|
||||
"device_id": heatchill_id,
|
||||
"action_name": "heat_chill_stop",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel_id # 🔧 使用 vessel_id
|
||||
"vessel": {"id": vessel_id}, # 🔧 使用 vessel_id
|
||||
}
|
||||
}
|
||||
action_sequence.append(heatchill_stop_action)
|
||||
|
||||
@@ -563,7 +563,7 @@ def generate_dissolve_protocol(
|
||||
"device_id": heatchill_id,
|
||||
"action_name": "heat_chill_start",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel_id,
|
||||
"vessel": {"id": vessel_id},
|
||||
"temp": final_temp,
|
||||
"purpose": f"溶解准备 - {event}" if event else "溶解准备"
|
||||
}
|
||||
@@ -587,7 +587,7 @@ def generate_dissolve_protocol(
|
||||
"device_id": stirrer_id,
|
||||
"action_name": "start_stir",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel_id,
|
||||
"vessel": {"id": vessel_id},
|
||||
"stir_speed": stir_speed,
|
||||
"purpose": f"溶解搅拌 - {event}" if event else "溶解搅拌"
|
||||
}
|
||||
@@ -612,7 +612,7 @@ def generate_dissolve_protocol(
|
||||
|
||||
# 固体加样
|
||||
add_kwargs = {
|
||||
"vessel": vessel_id,
|
||||
"vessel": {"id": vessel_id},
|
||||
"reagent": reagent or amount or "solid reagent",
|
||||
"purpose": f"溶解固体试剂 - {event}" if event else "溶解固体试剂",
|
||||
"event": event
|
||||
@@ -758,7 +758,7 @@ def generate_dissolve_protocol(
|
||||
"device_id": heatchill_id,
|
||||
"action_name": "heat_chill",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel_id,
|
||||
"vessel": {"id": vessel_id},
|
||||
"temp": final_temp,
|
||||
"time": final_time,
|
||||
"stir": True,
|
||||
@@ -776,7 +776,7 @@ def generate_dissolve_protocol(
|
||||
"device_id": stirrer_id,
|
||||
"action_name": "stir",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel_id,
|
||||
"vessel": {"id": vessel_id},
|
||||
"stir_time": final_time,
|
||||
"stir_speed": stir_speed,
|
||||
"settling_time": 0,
|
||||
@@ -802,7 +802,7 @@ def generate_dissolve_protocol(
|
||||
"device_id": heatchill_id,
|
||||
"action_name": "heat_chill_stop",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel_id
|
||||
"vessel": {"id": vessel_id},
|
||||
}
|
||||
}
|
||||
action_sequence.append(stop_action)
|
||||
|
||||
@@ -167,7 +167,7 @@ def generate_dry_protocol(
|
||||
"device_id": heater_id,
|
||||
"action_name": "heat_chill_start",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel_id, # 🔧 使用 vessel_id
|
||||
"vessel": {"id": vessel_id}, # 🔧 使用 vessel_id
|
||||
"temp": dry_temp,
|
||||
"purpose": f"干燥 {compound or '化合物'}"
|
||||
}
|
||||
@@ -191,7 +191,7 @@ def generate_dry_protocol(
|
||||
"device_id": heater_id,
|
||||
"action_name": "heat_chill",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel_id, # 🔧 使用 vessel_id
|
||||
"vessel": {"id": vessel_id}, # 🔧 使用 vessel_id
|
||||
"temp": dry_temp,
|
||||
"time": simulation_time,
|
||||
"purpose": f"干燥 {compound or '化合物'},保持温度 {dry_temp}°C"
|
||||
@@ -251,7 +251,7 @@ def generate_dry_protocol(
|
||||
"device_id": heater_id,
|
||||
"action_name": "heat_chill_stop",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel_id, # 🔧 使用 vessel_id
|
||||
"vessel": {"id": vessel_id}, # 🔧 使用 vessel_id
|
||||
"purpose": f"干燥完成,停止加热"
|
||||
}
|
||||
})
|
||||
|
||||
@@ -452,7 +452,7 @@ def generate_evacuateandrefill_protocol(
|
||||
"device_id": stirrer_id,
|
||||
"action_name": "start_stir",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel_id, # 🔧 使用 vessel_id
|
||||
"vessel": {"id": vessel_id}, # 🔧 使用 vessel_id
|
||||
"stir_speed": STIR_SPEED,
|
||||
"purpose": "抽真空充气前预搅拌"
|
||||
}
|
||||
@@ -685,7 +685,7 @@ def generate_evacuateandrefill_protocol(
|
||||
action_sequence.append({
|
||||
"device_id": stirrer_id,
|
||||
"action_name": "stop_stir",
|
||||
"action_kwargs": {"vessel": vessel_id} # 🔧 使用 vessel_id
|
||||
"action_kwargs": {"vessel": {"id": vessel_id},} # 🔧 使用 vessel_id
|
||||
})
|
||||
else:
|
||||
action_sequence.append(create_action_log("跳过搅拌器停止", "⏭️"))
|
||||
|
||||
@@ -329,7 +329,7 @@ def generate_evaporate_protocol(
|
||||
"device_id": rotavap_device,
|
||||
"action_name": "evaporate",
|
||||
"action_kwargs": {
|
||||
"vessel": target_vessel,
|
||||
"vessel": {"id": target_vessel},
|
||||
"pressure": float(pressure),
|
||||
"temp": float(temp),
|
||||
"time": float(final_time), # 🔧 强制转换为float类型
|
||||
|
||||
@@ -220,7 +220,7 @@ def generate_heat_chill_protocol(
|
||||
"device_id": heatchill_id,
|
||||
"action_name": "heat_chill",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel,
|
||||
"vessel": {"id": vessel},
|
||||
"temp": float(final_temp),
|
||||
"time": float(final_time),
|
||||
"stir": bool(stir),
|
||||
@@ -287,7 +287,8 @@ def generate_heat_chill_start_protocol(
|
||||
"action_name": "heat_chill_start",
|
||||
"action_kwargs": {
|
||||
"temp": temp,
|
||||
"purpose": purpose or f"开始加热到 {temp}°C"
|
||||
"purpose": purpose or f"开始加热到 {temp}°C",
|
||||
"vessel": {"id": vessel_id},
|
||||
}
|
||||
}]
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import traceback
|
||||
import traceback
|
||||
import numpy as np
|
||||
import networkx as nx
|
||||
import asyncio
|
||||
@@ -10,18 +9,12 @@ import sys
|
||||
|
||||
from unilabos.compile.utils.vessel_parser import get_vessel
|
||||
|
||||
from unilabos.compile.utils.vessel_parser import get_vessel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def debug_print(message):
|
||||
"""强制输出调试信息"""
|
||||
timestamp = time_module.strftime("%H:%M:%S")
|
||||
output = f"[{timestamp}] {message}"
|
||||
print(output, flush=True)
|
||||
sys.stdout.flush()
|
||||
# 同时写入日志
|
||||
output = f"[TRANSFER] {message}"
|
||||
logger.info(output)
|
||||
|
||||
|
||||
@@ -131,7 +124,6 @@ def find_connected_pump(G, valve_node):
|
||||
for neighbor in G.neighbors(valve_node):
|
||||
neighbor_class = G.nodes[neighbor].get("class", "") or ""
|
||||
# 排除非 电磁阀 和 泵 的邻居
|
||||
# 排除非 电磁阀 和 泵 的邻居
|
||||
debug_print(f" - 检查邻居 {neighbor}, class: {neighbor_class}")
|
||||
if "pump" in neighbor_class.lower():
|
||||
debug_print(f" ✅ 找到直接相连的泵: {neighbor}")
|
||||
@@ -242,9 +234,6 @@ def generate_pump_protocol(
|
||||
|
||||
# 验证容器存在
|
||||
debug_print(f"🔍 验证源容器 '{from_vessel_id}' 和目标容器 '{to_vessel_id}' 是否存在...")
|
||||
if from_vessel_id not in G.nodes():
|
||||
logger.error(f"源容器 '{from_vessel_id}' 不存在")
|
||||
debug_print(f"🔍 验证源容器 '{from_vessel_id}' 和目标容器 '{to_vessel_id}' 是否存在...")
|
||||
if from_vessel_id not in G.nodes():
|
||||
logger.error(f"源容器 '{from_vessel_id}' 不存在")
|
||||
return pump_action_sequence
|
||||
@@ -256,10 +245,7 @@ def generate_pump_protocol(
|
||||
try:
|
||||
shortest_path = nx.shortest_path(G, source=from_vessel_id, target=to_vessel_id)
|
||||
debug_print(f"PUMP_TRANSFER: 路径 {from_vessel_id} -> {to_vessel_id}: {shortest_path}")
|
||||
shortest_path = nx.shortest_path(G, source=from_vessel_id, target=to_vessel_id)
|
||||
debug_print(f"PUMP_TRANSFER: 路径 {from_vessel_id} -> {to_vessel_id}: {shortest_path}")
|
||||
except nx.NetworkXNoPath:
|
||||
logger.error(f"无法找到从 '{from_vessel_id}' 到 '{to_vessel_id}' 的路径")
|
||||
logger.error(f"无法找到从 '{from_vessel_id}' 到 '{to_vessel_id}' 的路径")
|
||||
return pump_action_sequence
|
||||
|
||||
@@ -267,7 +253,6 @@ def generate_pump_protocol(
|
||||
pump_backbone = []
|
||||
for node in shortest_path:
|
||||
# 跳过起始和结束容器
|
||||
if node == from_vessel_id or node == to_vessel_id:
|
||||
if node == from_vessel_id or node == to_vessel_id:
|
||||
continue
|
||||
|
||||
@@ -370,11 +355,9 @@ def generate_pump_protocol(
|
||||
return "default"
|
||||
|
||||
# 从源容器吸液
|
||||
if not from_vessel_id.startswith("pump") and pump_backbone:
|
||||
if not from_vessel_id.startswith("pump") and pump_backbone:
|
||||
first_pump_node = pump_backbone[0]
|
||||
if first_pump_node in valve_from_node and first_pump_node in pumps_from_node:
|
||||
port_command = get_safe_edge_data(first_pump_node, from_vessel_id, first_pump_node)
|
||||
port_command = get_safe_edge_data(first_pump_node, from_vessel_id, first_pump_node)
|
||||
pump_action_sequence.extend([
|
||||
{
|
||||
@@ -438,11 +421,9 @@ def generate_pump_protocol(
|
||||
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}})
|
||||
|
||||
# 排液到目标容器
|
||||
if not to_vessel_id.startswith("pump") and pump_backbone:
|
||||
if not to_vessel_id.startswith("pump") and pump_backbone:
|
||||
last_pump_node = pump_backbone[-1]
|
||||
if last_pump_node in valve_from_node and last_pump_node in pumps_from_node:
|
||||
port_command = get_safe_edge_data(last_pump_node, to_vessel_id, last_pump_node)
|
||||
port_command = get_safe_edge_data(last_pump_node, to_vessel_id, last_pump_node)
|
||||
pump_action_sequence.extend([
|
||||
{
|
||||
@@ -514,7 +495,6 @@ def generate_pump_protocol_with_rinsing(
|
||||
debug_print("=" * 60)
|
||||
debug_print(f"PUMP_TRANSFER: 🚀 开始生成协议 (同步版本)")
|
||||
debug_print(f" 📍 路径: {from_vessel_id} -> {to_vessel_id}")
|
||||
debug_print(f" 📍 路径: {from_vessel_id} -> {to_vessel_id}")
|
||||
debug_print(f" 🕐 时间戳: {time_module.time()}")
|
||||
debug_print(f" 🔒 获得执行锁")
|
||||
debug_print("=" * 60)
|
||||
@@ -557,7 +537,6 @@ def generate_pump_protocol_with_rinsing(
|
||||
elif parsed_volume == 0.0 and amount.lower().strip() == "all":
|
||||
debug_print("🎯 检测到 amount='all',从容器读取全部体积...")
|
||||
actual_volume = get_vessel_liquid_volume(G, from_vessel_id)
|
||||
actual_volume = get_vessel_liquid_volume(G, from_vessel_id)
|
||||
if actual_volume > 0:
|
||||
final_volume = actual_volume
|
||||
debug_print(f"✅ amount='all',设置体积为: {final_volume}mL")
|
||||
@@ -653,7 +632,6 @@ def generate_pump_protocol_with_rinsing(
|
||||
f" - 参数: G, '{from_vessel_id}', '{to_vessel_id}', {final_volume}, {final_flowrate}, {final_transfer_flowrate}")
|
||||
|
||||
pump_action_sequence = generate_pump_protocol(
|
||||
G, from_vessel_id, to_vessel_id, final_volume,
|
||||
G, from_vessel_id, to_vessel_id, final_volume,
|
||||
final_flowrate, final_transfer_flowrate
|
||||
)
|
||||
@@ -669,7 +647,6 @@ def generate_pump_protocol_with_rinsing(
|
||||
|
||||
if from_vessel_id in G.nodes() and to_vessel_id in G.nodes():
|
||||
try:
|
||||
path = nx.shortest_path(G, source=from_vessel_id, target=to_vessel_id)
|
||||
path = nx.shortest_path(G, source=from_vessel_id, target=to_vessel_id)
|
||||
debug_print(f" - 路径存在: {path}")
|
||||
except Exception as path_error:
|
||||
@@ -681,7 +658,6 @@ def generate_pump_protocol_with_rinsing(
|
||||
"action_name": "log_message",
|
||||
"action_kwargs": {
|
||||
"message": f"⚠️ 路径问题,无法转移: {final_volume}mL 从 {from_vessel_id} 到 {to_vessel_id}"
|
||||
"message": f"⚠️ 路径问题,无法转移: {final_volume}mL 从 {from_vessel_id} 到 {to_vessel_id}"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -706,7 +682,6 @@ def generate_pump_protocol_with_rinsing(
|
||||
"action_name": "log_message",
|
||||
"action_kwargs": {
|
||||
"message": f"❌ 转移失败: {final_volume}mL 从 {from_vessel_id} 到 {to_vessel_id}, 错误: {str(e)}"
|
||||
"message": f"❌ 转移失败: {final_volume}mL 从 {from_vessel_id} 到 {to_vessel_id}, 错误: {str(e)}"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -828,7 +803,6 @@ def _generate_rinsing_sequence(
|
||||
rinsing_actions = []
|
||||
|
||||
try:
|
||||
shortest_path = nx.shortest_path(G, source=from_vessel_id, target=to_vessel_id)
|
||||
shortest_path = nx.shortest_path(G, source=from_vessel_id, target=to_vessel_id)
|
||||
pump_backbone = shortest_path[1:-1]
|
||||
|
||||
@@ -884,7 +858,6 @@ def _generate_rinsing_sequence(
|
||||
)
|
||||
rinsing_actions.extend(
|
||||
generate_pump_protocol(G, solvent_vessel, to_vessel_id, rinsing_volume, flowrate, transfer_flowrate)
|
||||
generate_pump_protocol(G, solvent_vessel, to_vessel_id, rinsing_volume, flowrate, transfer_flowrate)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -909,13 +882,11 @@ def _generate_air_rinsing_sequence(G: nx.DiGraph, from_vessel_id: str, to_vessel
|
||||
# 空气冲洗源容器
|
||||
air_rinsing_actions.extend(
|
||||
generate_pump_protocol(G, air_vessel, from_vessel_id, rinsing_volume, flowrate, transfer_flowrate)
|
||||
generate_pump_protocol(G, air_vessel, from_vessel_id, rinsing_volume, flowrate, transfer_flowrate)
|
||||
)
|
||||
|
||||
# 空气冲洗目标容器
|
||||
air_rinsing_actions.extend(
|
||||
generate_pump_protocol(G, air_vessel, to_vessel_id, rinsing_volume, flowrate, transfer_flowrate)
|
||||
generate_pump_protocol(G, air_vessel, to_vessel_id, rinsing_volume, flowrate, transfer_flowrate)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -2,6 +2,7 @@ from typing import List, Dict, Any, Union
|
||||
import networkx as nx
|
||||
import logging
|
||||
import re
|
||||
from .utils.vessel_parser import get_vessel
|
||||
from .pump_protocol import generate_pump_protocol_with_rinsing
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -403,9 +404,9 @@ def generate_run_column_protocol(
|
||||
"""
|
||||
|
||||
# 🔧 核心修改:从字典中提取容器ID
|
||||
from_vessel_id = from_vessel["id"]
|
||||
to_vessel_id = to_vessel["id"]
|
||||
|
||||
from_vessel_id, _ = get_vessel(from_vessel)
|
||||
to_vessel_id, _ = get_vessel(to_vessel)
|
||||
|
||||
debug_print("🏛️" * 20)
|
||||
debug_print("🚀 开始生成柱层析协议(支持vessel字典和体积运算)✨")
|
||||
debug_print(f"📝 输入参数:")
|
||||
@@ -772,8 +773,8 @@ def generate_gradient_column_protocol(G: nx.DiGraph, from_vessel: dict, to_vesse
|
||||
column: str, start_ratio: str = "10:90",
|
||||
end_ratio: str = "50:50") -> List[Dict[str, Any]]:
|
||||
"""梯度洗脱柱层析(中等比例)"""
|
||||
from_vessel_id = from_vessel["id"]
|
||||
to_vessel_id = to_vessel["id"]
|
||||
from_vessel_id, _ = get_vessel(from_vessel)
|
||||
to_vessel_id, _ = get_vessel(to_vessel)
|
||||
debug_print(f"📈 梯度柱层析: {from_vessel_id} → {to_vessel_id} ({start_ratio} → {end_ratio})")
|
||||
# 使用中间比例作为近似
|
||||
return generate_run_column_protocol(G, from_vessel, to_vessel, column, ratio="30:70")
|
||||
@@ -781,8 +782,8 @@ def generate_gradient_column_protocol(G: nx.DiGraph, from_vessel: dict, to_vesse
|
||||
def generate_polar_column_protocol(G: nx.DiGraph, from_vessel: dict, to_vessel: dict,
|
||||
column: str) -> List[Dict[str, Any]]:
|
||||
"""极性化合物柱层析(高极性溶剂比例)"""
|
||||
from_vessel_id = from_vessel["id"]
|
||||
to_vessel_id = to_vessel["id"]
|
||||
from_vessel_id, _ = get_vessel(from_vessel)
|
||||
to_vessel_id, _ = get_vessel(to_vessel)
|
||||
debug_print(f"⚡ 极性化合物柱层析: {from_vessel_id} → {to_vessel_id}")
|
||||
return generate_run_column_protocol(G, from_vessel, to_vessel, column,
|
||||
solvent1="ethyl_acetate", solvent2="hexane", ratio="70:30")
|
||||
@@ -790,8 +791,8 @@ def generate_polar_column_protocol(G: nx.DiGraph, from_vessel: dict, to_vessel:
|
||||
def generate_nonpolar_column_protocol(G: nx.DiGraph, from_vessel: dict, to_vessel: dict,
|
||||
column: str) -> List[Dict[str, Any]]:
|
||||
"""非极性化合物柱层析(低极性溶剂比例)"""
|
||||
from_vessel_id = from_vessel["id"]
|
||||
to_vessel_id = to_vessel["id"]
|
||||
from_vessel_id, _ = get_vessel(from_vessel)
|
||||
to_vessel_id, _ = get_vessel(to_vessel)
|
||||
debug_print(f"🛢️ 非极性化合物柱层析: {from_vessel_id} → {to_vessel_id}")
|
||||
return generate_run_column_protocol(G, from_vessel, to_vessel, column,
|
||||
solvent1="ethyl_acetate", solvent2="hexane", ratio="5:95")
|
||||
@@ -804,4 +805,3 @@ def test_run_column_protocol():
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_run_column_protocol()
|
||||
|
||||
|
||||
@@ -265,7 +265,7 @@ def generate_separate_protocol(
|
||||
"device_id": stirrer_device,
|
||||
"action_name": "start_stir",
|
||||
"action_kwargs": {
|
||||
"vessel": final_vessel_id, # 🔧 使用 final_vessel_id
|
||||
"vessel": {"id": final_vessel_id}, # 🔧 使用 final_vessel_id
|
||||
"stir_speed": stir_speed,
|
||||
"purpose": f"分离混合 - {purpose}"
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ def generate_stir_protocol(
|
||||
"action_name": "stir",
|
||||
"action_kwargs": {
|
||||
# 🔧 关键修复:传递vessel_id字符串,而不是完整的Resource对象
|
||||
"vessel": vessel_id, # 传递字符串ID,不是Resource对象
|
||||
"vessel": {"id": vessel_id}, # 传递字符串ID,不是Resource对象
|
||||
"time": str(time),
|
||||
"event": event,
|
||||
"time_spec": time_spec,
|
||||
@@ -323,7 +323,7 @@ def generate_start_stir_protocol(
|
||||
"action_name": "start_stir",
|
||||
"action_kwargs": {
|
||||
# 🔧 关键修复:传递vessel_id字符串,而不是完整的Resource对象
|
||||
"vessel": vessel_id, # 传递字符串ID,不是Resource对象
|
||||
"vessel": {"id": vessel_id}, # 传递字符串ID,不是Resource对象
|
||||
"stir_speed": stir_speed,
|
||||
"purpose": purpose or f"启动搅拌 {stir_speed} RPM"
|
||||
}
|
||||
@@ -383,7 +383,7 @@ def generate_stop_stir_protocol(
|
||||
"action_name": "stop_stir",
|
||||
"action_kwargs": {
|
||||
# 🔧 关键修复:传递vessel_id字符串,而不是完整的Resource对象
|
||||
"vessel": vessel_id # 传递字符串ID,不是Resource对象
|
||||
"vessel": {"id": vessel_id}, # 传递字符串ID,不是Resource对象
|
||||
}
|
||||
}]
|
||||
|
||||
|
||||
@@ -361,7 +361,7 @@ def generate_wash_solid_protocol(
|
||||
"device_id": "stirrer_1",
|
||||
"action_name": "stir",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel_id, # 🔧 使用 vessel_id
|
||||
"vessel": {"id": vessel_id}, # 🔧 使用 vessel_id
|
||||
"time": str(time),
|
||||
"stir_time": final_time,
|
||||
"stir_speed": stir_speed,
|
||||
@@ -377,7 +377,7 @@ def generate_wash_solid_protocol(
|
||||
"device_id": "filter_1",
|
||||
"action_name": "filter",
|
||||
"action_kwargs": {
|
||||
"vessel": vessel_id, # 🔧 使用 vessel_id
|
||||
"vessel": {"id": vessel_id}, # 🔧 使用 vessel_id
|
||||
"filtrate_vessel": actual_filtrate_vessel,
|
||||
"temp": temp,
|
||||
"volume": final_volume
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env python
|
||||
# coding=utf-8
|
||||
# 定义配置变量和加载函数
|
||||
import base64
|
||||
import traceback
|
||||
import os
|
||||
import importlib.util
|
||||
from typing import Optional
|
||||
from unilabos.utils import logger
|
||||
|
||||
|
||||
class BasicConfig:
|
||||
ENV = "pro" # 'test'
|
||||
ak = ""
|
||||
sk = ""
|
||||
working_dir = ""
|
||||
config_path = ""
|
||||
is_host_mode = True
|
||||
@@ -17,25 +17,22 @@ class BasicConfig:
|
||||
machine_name = "undefined"
|
||||
vis_2d_enable = False
|
||||
enable_resource_load = True
|
||||
communication_protocol = "websocket"
|
||||
|
||||
@classmethod
|
||||
def auth_secret(cls):
|
||||
if not cls.ak or not cls.sk:
|
||||
return ""
|
||||
target = f"{cls.ak}:{cls.sk}"
|
||||
base64_target = base64.b64encode(target.encode("utf-8")).decode("utf-8")
|
||||
return base64_target
|
||||
|
||||
|
||||
# MQTT配置
|
||||
class MQConfig:
|
||||
lab_id = ""
|
||||
instance_id = ""
|
||||
access_key = ""
|
||||
secret_key = ""
|
||||
group_id = ""
|
||||
broker_url = ""
|
||||
port = 1883
|
||||
ca_content = ""
|
||||
cert_content = ""
|
||||
key_content = ""
|
||||
|
||||
# 指定
|
||||
ca_file = "" # 相对config.py所在目录的路径
|
||||
cert_file = "" # 相对config.py所在目录的路径
|
||||
key_file = "" # 相对config.py所在目录的路径
|
||||
# WebSocket配置
|
||||
class WSConfig:
|
||||
reconnect_interval = 5 # 重连间隔(秒)
|
||||
max_reconnect_attempts = 999 # 最大重连次数
|
||||
ping_interval = 30 # ping间隔(秒)
|
||||
|
||||
|
||||
# OSS上传配置
|
||||
@@ -65,48 +62,13 @@ class ROSConfig:
|
||||
]
|
||||
|
||||
|
||||
def _update_config_from_module(module, override_labid: str):
|
||||
def _update_config_from_module(module):
|
||||
for name, obj in globals().items():
|
||||
if isinstance(obj, type) and name.endswith("Config"):
|
||||
if hasattr(module, name) and isinstance(getattr(module, name), type):
|
||||
for attr in dir(getattr(module, name)):
|
||||
if not attr.startswith("_"):
|
||||
setattr(obj, attr, getattr(getattr(module, name), attr))
|
||||
# 更新OSS认证
|
||||
if len(OSSUploadConfig.authorization) == 0:
|
||||
OSSUploadConfig.authorization = f"lab {MQConfig.lab_id}"
|
||||
# 对 ca_file cert_file key_file 进行初始化
|
||||
if override_labid:
|
||||
MQConfig.lab_id = override_labid
|
||||
logger.warning(f"[ENV] 当前实验室启动的ID被设置为:{override_labid}")
|
||||
if len(MQConfig.ca_content) == 0:
|
||||
# 需要先判断是否为相对路径
|
||||
if MQConfig.ca_file.startswith("."):
|
||||
MQConfig.ca_file = os.path.join(BasicConfig.config_path, MQConfig.ca_file)
|
||||
if len(MQConfig.ca_file) != 0:
|
||||
with open(MQConfig.ca_file, "r", encoding="utf-8") as f:
|
||||
MQConfig.ca_content = f.read()
|
||||
else:
|
||||
logger.warning("Skipping CA file loading, ca_file is empty")
|
||||
if len(MQConfig.cert_content) == 0:
|
||||
# 需要先判断是否为相对路径
|
||||
if MQConfig.cert_file.startswith("."):
|
||||
MQConfig.cert_file = os.path.join(BasicConfig.config_path, MQConfig.cert_file)
|
||||
if len(MQConfig.ca_file) != 0:
|
||||
with open(MQConfig.cert_file, "r", encoding="utf-8") as f:
|
||||
MQConfig.cert_content = f.read()
|
||||
else:
|
||||
logger.warning("Skipping cert file loading, cert_file is empty")
|
||||
if len(MQConfig.key_content) == 0:
|
||||
# 需要先判断是否为相对路径
|
||||
if MQConfig.key_file.startswith("."):
|
||||
MQConfig.key_file = os.path.join(BasicConfig.config_path, MQConfig.key_file)
|
||||
if len(MQConfig.ca_file) != 0:
|
||||
with open(MQConfig.key_file, "r", encoding="utf-8") as f:
|
||||
MQConfig.key_content = f.read()
|
||||
else:
|
||||
logger.warning("Skipping key file loading, key_file is empty")
|
||||
|
||||
|
||||
def _update_config_from_env():
|
||||
prefix = "UNILABOS_"
|
||||
@@ -159,8 +121,7 @@ def _update_config_from_env():
|
||||
logger.warning(f"[ENV] 解析环境变量 {env_key} 失败: {e}")
|
||||
|
||||
|
||||
|
||||
def load_config(config_path=None, override_labid=None):
|
||||
def load_config(config_path=None):
|
||||
# 如果提供了配置文件路径,从该文件导入配置
|
||||
if config_path:
|
||||
env_config_path = os.environ.get("UNILABOS_BASICCONFIG_CONFIG_PATH")
|
||||
@@ -177,7 +138,7 @@ def load_config(config_path=None, override_labid=None):
|
||||
return
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module) # type: ignore
|
||||
_update_config_from_module(module, override_labid)
|
||||
_update_config_from_module(module)
|
||||
logger.info(f"[ENV] 配置文件 {config_path} 加载成功")
|
||||
_update_config_from_env()
|
||||
except Exception as e:
|
||||
@@ -186,4 +147,4 @@ def load_config(config_path=None, override_labid=None):
|
||||
exit(1)
|
||||
else:
|
||||
config_path = os.path.join(os.path.dirname(__file__), "local_config.py")
|
||||
load_config(config_path, override_labid)
|
||||
load_config(config_path)
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
# MQTT配置
|
||||
class MQConfig:
|
||||
lab_id = ""
|
||||
instance_id = ""
|
||||
access_key = ""
|
||||
secret_key = ""
|
||||
group_id = ""
|
||||
broker_url = ""
|
||||
port = 1883
|
||||
# unilabos的配置文件
|
||||
|
||||
ca_file = "./CA.crt"
|
||||
cert_file = "./lab.crt"
|
||||
key_file = "./lab.key"
|
||||
class BasicConfig:
|
||||
ak = "" # 实验室网页给您提供的ak代码,您可以在配置文件中指定,也可以通过运行unilabos时以 --ak 传入,优先按照传入参数解析
|
||||
sk = "" # 实验室网页给您提供的sk代码,您可以在配置文件中指定,也可以通过运行unilabos时以 --sk 传入,优先按照传入参数解析
|
||||
|
||||
# HTTP配置
|
||||
class HTTPConfig:
|
||||
remote_addr = "https://uni-lab.bohrium.com/api/v1"
|
||||
|
||||
# WebSocket配置,一般无需调整
|
||||
class WSConfig:
|
||||
reconnect_interval = 5 # 重连间隔(秒)
|
||||
max_reconnect_attempts = 999 # 最大重连次数
|
||||
ping_interval = 30 # ping间隔(秒)
|
||||
@@ -1 +0,0 @@
|
||||
from .eis_model import EISModelBasedController
|
||||
@@ -1,5 +0,0 @@
|
||||
import numpy as np
|
||||
|
||||
|
||||
def EISModelBasedController(eis: np.array) -> float:
|
||||
return 0.0
|
||||
454
unilabos/device_comms/coin_cell_assembly_workstation.py
Normal file
454
unilabos/device_comms/coin_cell_assembly_workstation.py
Normal file
@@ -0,0 +1,454 @@
|
||||
"""
|
||||
纽扣电池组装工作站
|
||||
Coin Cell Assembly Workstation
|
||||
|
||||
继承工作站基类,实现纽扣电池特定功能
|
||||
"""
|
||||
from typing import Dict, Any, List, Optional, Union
|
||||
|
||||
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
|
||||
from unilabos.device_comms.workstation_base import WorkstationBase, WorkflowInfo
|
||||
from unilabos.device_comms.workstation_communication import (
|
||||
WorkstationCommunicationBase, CommunicationConfig, CommunicationProtocol, CoinCellCommunication
|
||||
)
|
||||
from unilabos.device_comms.workstation_material_management import (
|
||||
MaterialManagementBase, CoinCellMaterialManagement
|
||||
)
|
||||
from unilabos.utils.log import logger
|
||||
|
||||
|
||||
class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||
"""纽扣电池组装工作站
|
||||
|
||||
基于工作站基类,实现纽扣电池制造的特定功能:
|
||||
1. 纽扣电池特定的通信协议
|
||||
2. 纽扣电池物料管理(料板、极片、电池等)
|
||||
3. 电池制造工作流
|
||||
4. 质量检查工作流
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device_id: str,
|
||||
children: Dict[str, Dict[str, Any]],
|
||||
protocol_type: Union[str, List[str]] = "BatteryManufacturingProtocol",
|
||||
resource_tracker: Optional[DeviceNodeResourceTracker] = None,
|
||||
modbus_config: Optional[Dict[str, Any]] = None,
|
||||
deck_config: Optional[Dict[str, Any]] = None,
|
||||
csv_path: str = "./coin_cell_assembly.csv",
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
# 设置通信配置
|
||||
modbus_config = modbus_config or {"host": "127.0.0.1", "port": 5021}
|
||||
self.communication_config = CommunicationConfig(
|
||||
protocol=CommunicationProtocol.MODBUS_TCP,
|
||||
host=modbus_config["host"],
|
||||
port=modbus_config["port"],
|
||||
timeout=modbus_config.get("timeout", 5.0),
|
||||
retry_count=modbus_config.get("retry_count", 3)
|
||||
)
|
||||
|
||||
# 设置台面配置
|
||||
self.deck_config = deck_config or {
|
||||
"size_x": 1620.0,
|
||||
"size_y": 1270.0,
|
||||
"size_z": 500.0
|
||||
}
|
||||
|
||||
# CSV地址映射文件路径
|
||||
self.csv_path = csv_path
|
||||
|
||||
# 创建资源跟踪器(如果没有提供)
|
||||
if resource_tracker is None:
|
||||
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
|
||||
resource_tracker = DeviceNodeResourceTracker()
|
||||
|
||||
# 初始化基类
|
||||
super().__init__(
|
||||
device_id=device_id,
|
||||
children=children,
|
||||
protocol_type=protocol_type,
|
||||
resource_tracker=resource_tracker,
|
||||
communication_config=self.communication_config,
|
||||
deck_config=self.deck_config,
|
||||
*args,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
logger.info(f"纽扣电池组装工作站 {device_id} 初始化完成")
|
||||
|
||||
def _create_communication_module(self) -> WorkstationCommunicationBase:
|
||||
"""创建纽扣电池通信模块"""
|
||||
return CoinCellCommunication(
|
||||
communication_config=self.communication_config,
|
||||
csv_path=self.csv_path
|
||||
)
|
||||
|
||||
def _create_material_management_module(self) -> MaterialManagementBase:
|
||||
"""创建纽扣电池物料管理模块"""
|
||||
return CoinCellMaterialManagement(
|
||||
device_id=self.device_id,
|
||||
deck_config=self.deck_config,
|
||||
resource_tracker=self.resource_tracker,
|
||||
children_config=self.children
|
||||
)
|
||||
|
||||
def _register_supported_workflows(self):
|
||||
"""注册纽扣电池工作流"""
|
||||
# 电池制造工作流
|
||||
self.supported_workflows["battery_manufacturing"] = WorkflowInfo(
|
||||
name="battery_manufacturing",
|
||||
description="纽扣电池制造工作流",
|
||||
estimated_duration=300.0, # 5分钟
|
||||
required_materials=["cathode_sheet", "anode_sheet", "separator", "electrolyte"],
|
||||
output_product="coin_cell_battery",
|
||||
parameters_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"electrolyte_num": {
|
||||
"type": "integer",
|
||||
"description": "电解液瓶数",
|
||||
"minimum": 1,
|
||||
"maximum": 32
|
||||
},
|
||||
"electrolyte_volume": {
|
||||
"type": "number",
|
||||
"description": "电解液体积 (μL)",
|
||||
"minimum": 0.1,
|
||||
"maximum": 100.0
|
||||
},
|
||||
"assembly_pressure": {
|
||||
"type": "number",
|
||||
"description": "组装压力 (N)",
|
||||
"minimum": 100.0,
|
||||
"maximum": 5000.0
|
||||
},
|
||||
"cathode_material": {
|
||||
"type": "string",
|
||||
"description": "正极材料类型",
|
||||
"enum": ["LiFePO4", "LiCoO2", "NCM", "LMO"]
|
||||
},
|
||||
"anode_material": {
|
||||
"type": "string",
|
||||
"description": "负极材料类型",
|
||||
"enum": ["Graphite", "LTO", "Silicon"]
|
||||
}
|
||||
},
|
||||
"required": ["electrolyte_num", "electrolyte_volume", "assembly_pressure"]
|
||||
}
|
||||
)
|
||||
|
||||
# 质量检查工作流
|
||||
self.supported_workflows["quality_inspection"] = WorkflowInfo(
|
||||
name="quality_inspection",
|
||||
description="产品质量检查工作流",
|
||||
estimated_duration=60.0, # 1分钟
|
||||
required_materials=["finished_battery"],
|
||||
output_product="quality_report",
|
||||
parameters_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"test_voltage": {
|
||||
"type": "boolean",
|
||||
"description": "是否测试电压",
|
||||
"default": True
|
||||
},
|
||||
"test_capacity": {
|
||||
"type": "boolean",
|
||||
"description": "是否测试容量",
|
||||
"default": False
|
||||
},
|
||||
"voltage_threshold": {
|
||||
"type": "number",
|
||||
"description": "电压阈值 (V)",
|
||||
"minimum": 2.0,
|
||||
"maximum": 4.5,
|
||||
"default": 3.0
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# 设备初始化工作流
|
||||
self.supported_workflows["device_initialization"] = WorkflowInfo(
|
||||
name="device_initialization",
|
||||
description="设备初始化工作流",
|
||||
estimated_duration=30.0, # 30秒
|
||||
required_materials=[],
|
||||
output_product="ready_status",
|
||||
parameters_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"auto_mode": {
|
||||
"type": "boolean",
|
||||
"description": "是否启用自动模式",
|
||||
"default": True
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# ============ 纽扣电池特定方法 ============
|
||||
|
||||
def get_electrode_sheet_inventory(self) -> Dict[str, int]:
|
||||
"""获取极片库存统计"""
|
||||
try:
|
||||
sheets = self.material_management.find_electrode_sheets()
|
||||
inventory = {}
|
||||
|
||||
for sheet in sheets:
|
||||
material_type = getattr(sheet, 'material_type', 'unknown')
|
||||
inventory[material_type] = inventory.get(material_type, 0) + 1
|
||||
|
||||
return inventory
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取极片库存失败: {e}")
|
||||
return {}
|
||||
|
||||
def get_battery_production_statistics(self) -> Dict[str, Any]:
|
||||
"""获取电池生产统计"""
|
||||
try:
|
||||
production_data = self.communication.get_production_data()
|
||||
|
||||
# 添加物料统计
|
||||
electrode_inventory = self.get_electrode_sheet_inventory()
|
||||
battery_count = len(self.material_management.find_batteries())
|
||||
|
||||
return {
|
||||
**production_data,
|
||||
"electrode_inventory": electrode_inventory,
|
||||
"finished_battery_count": battery_count,
|
||||
"material_plates": len(self.material_management.find_material_plates()),
|
||||
"press_slots": len(self.material_management.find_press_slots())
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取生产统计失败: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
def create_new_battery(self, battery_spec: Dict[str, Any]) -> Optional[str]:
|
||||
"""创建新电池资源"""
|
||||
try:
|
||||
from unilabos.device_comms.button_battery_station import Battery
|
||||
import uuid
|
||||
|
||||
battery_id = f"battery_{uuid.uuid4().hex[:8]}"
|
||||
|
||||
battery = Battery(
|
||||
name=battery_id,
|
||||
diameter=battery_spec.get("diameter", 20.0),
|
||||
height=battery_spec.get("height", 3.2),
|
||||
max_volume=battery_spec.get("max_volume", 100.0),
|
||||
barcode=battery_spec.get("barcode", "")
|
||||
)
|
||||
|
||||
# 添加到物料管理系统
|
||||
self.material_management.plr_resources[battery_id] = battery
|
||||
self.material_management.resource_tracker.add_resource(battery)
|
||||
|
||||
logger.info(f"创建新电池资源: {battery_id}")
|
||||
return battery_id
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建电池资源失败: {e}")
|
||||
return None
|
||||
|
||||
def find_available_press_slot(self) -> Optional[str]:
|
||||
"""查找可用的压制槽"""
|
||||
try:
|
||||
press_slots = self.material_management.find_press_slots()
|
||||
|
||||
for slot in press_slots:
|
||||
if hasattr(slot, 'has_battery') and not slot.has_battery():
|
||||
return slot.name
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"查找可用压制槽失败: {e}")
|
||||
return None
|
||||
|
||||
def get_glove_box_environment(self) -> Dict[str, Any]:
|
||||
"""获取手套箱环境数据"""
|
||||
try:
|
||||
device_status = self.communication.get_device_status()
|
||||
environment = device_status.get("environment", {})
|
||||
|
||||
return {
|
||||
"pressure": environment.get("glove_box_pressure", 0.0),
|
||||
"o2_content": environment.get("o2_content", 0.0),
|
||||
"water_content": environment.get("water_content", 0.0),
|
||||
"is_safe": (
|
||||
environment.get("o2_content", 0.0) < 10.0 and # 氧气含量 < 10ppm
|
||||
environment.get("water_content", 0.0) < 1.0 # 水分含量 < 1ppm
|
||||
)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取手套箱环境失败: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
def start_data_export(self, file_path: str) -> bool:
|
||||
"""开始生产数据导出"""
|
||||
try:
|
||||
return self.communication.start_data_export(file_path, export_interval=5.0)
|
||||
except Exception as e:
|
||||
logger.error(f"启动数据导出失败: {e}")
|
||||
return False
|
||||
|
||||
def stop_data_export(self) -> bool:
|
||||
"""停止生产数据导出"""
|
||||
try:
|
||||
return self.communication.stop_data_export()
|
||||
except Exception as e:
|
||||
logger.error(f"停止数据导出失败: {e}")
|
||||
return False
|
||||
|
||||
# ============ 重写基类方法以支持纽扣电池特定功能 ============
|
||||
|
||||
def start_workflow(self, workflow_type: str, parameters: Dict[str, Any] = None) -> bool:
|
||||
"""启动工作流(重写以支持纽扣电池特定预处理)"""
|
||||
try:
|
||||
# 进行纽扣电池特定的预检查
|
||||
if workflow_type == "battery_manufacturing":
|
||||
# 检查手套箱环境
|
||||
env = self.get_glove_box_environment()
|
||||
if not env.get("is_safe", False):
|
||||
logger.error("手套箱环境不安全,无法启动电池制造工作流")
|
||||
return False
|
||||
|
||||
# 检查是否有可用的压制槽
|
||||
available_slot = self.find_available_press_slot()
|
||||
if not available_slot:
|
||||
logger.error("没有可用的压制槽,无法启动电池制造工作流")
|
||||
return False
|
||||
|
||||
# 检查极片库存
|
||||
electrode_inventory = self.get_electrode_sheet_inventory()
|
||||
if not electrode_inventory.get("cathode", 0) > 0 or not electrode_inventory.get("anode", 0) > 0:
|
||||
logger.error("极片库存不足,无法启动电池制造工作流")
|
||||
return False
|
||||
|
||||
# 调用基类方法
|
||||
return super().start_workflow(workflow_type, parameters)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"启动纽扣电池工作流失败: {e}")
|
||||
return False
|
||||
|
||||
# ============ 纽扣电池特定状态属性 ============
|
||||
|
||||
@property
|
||||
def electrode_sheet_count(self) -> int:
|
||||
"""极片总数"""
|
||||
try:
|
||||
return len(self.material_management.find_electrode_sheets())
|
||||
except:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def battery_count(self) -> int:
|
||||
"""电池总数"""
|
||||
try:
|
||||
return len(self.material_management.find_batteries())
|
||||
except:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def available_press_slots(self) -> int:
|
||||
"""可用压制槽数"""
|
||||
try:
|
||||
press_slots = self.material_management.find_press_slots()
|
||||
available = 0
|
||||
for slot in press_slots:
|
||||
if hasattr(slot, 'has_battery') and not slot.has_battery():
|
||||
available += 1
|
||||
return available
|
||||
except:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def environment_status(self) -> Dict[str, Any]:
|
||||
"""环境状态"""
|
||||
return self.get_glove_box_environment()
|
||||
|
||||
|
||||
# ============ 工厂函数 ============
|
||||
|
||||
def create_coin_cell_workstation(
|
||||
device_id: str,
|
||||
config_file: str,
|
||||
modbus_host: str = "127.0.0.1",
|
||||
modbus_port: int = 5021,
|
||||
csv_path: str = "./coin_cell_assembly.csv"
|
||||
) -> CoinCellAssemblyWorkstation:
|
||||
"""工厂函数:创建纽扣电池组装工作站
|
||||
|
||||
Args:
|
||||
device_id: 设备ID
|
||||
config_file: 配置文件路径(JSON格式)
|
||||
modbus_host: Modbus主机地址
|
||||
modbus_port: Modbus端口
|
||||
csv_path: 地址映射CSV文件路径
|
||||
|
||||
Returns:
|
||||
CoinCellAssemblyWorkstation: 工作站实例
|
||||
"""
|
||||
import json
|
||||
|
||||
try:
|
||||
# 加载配置文件
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
|
||||
# 提取配置
|
||||
children = config.get("children", {})
|
||||
deck_config = config.get("deck_config", {})
|
||||
|
||||
# 创建工作站
|
||||
workstation = CoinCellAssemblyWorkstation(
|
||||
device_id=device_id,
|
||||
children=children,
|
||||
modbus_config={
|
||||
"host": modbus_host,
|
||||
"port": modbus_port
|
||||
},
|
||||
deck_config=deck_config,
|
||||
csv_path=csv_path
|
||||
)
|
||||
|
||||
logger.info(f"纽扣电池工作站创建成功: {device_id}")
|
||||
return workstation
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建纽扣电池工作站失败: {e}")
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 示例用法
|
||||
workstation = create_coin_cell_workstation(
|
||||
device_id="coin_cell_station_01",
|
||||
config_file="./button_battery_workstation.json",
|
||||
modbus_host="127.0.0.1",
|
||||
modbus_port=5021
|
||||
)
|
||||
|
||||
# 启动电池制造工作流
|
||||
success = workstation.start_workflow(
|
||||
"battery_manufacturing",
|
||||
{
|
||||
"electrolyte_num": 16,
|
||||
"electrolyte_volume": 50.0,
|
||||
"assembly_pressure": 2000.0,
|
||||
"cathode_material": "LiFePO4",
|
||||
"anode_material": "Graphite"
|
||||
}
|
||||
)
|
||||
|
||||
if success:
|
||||
print("电池制造工作流启动成功")
|
||||
else:
|
||||
print("电池制造工作流启动失败")
|
||||
@@ -8,8 +8,8 @@ from pymodbus.client import ModbusSerialClient, ModbusTcpClient
|
||||
from pymodbus.framer import FramerType
|
||||
from typing import TypedDict
|
||||
|
||||
from unilabos.device_comms.modbus_plc.node.modbus import DeviceType, HoldRegister, Coil, InputRegister, DiscreteInputs, DataType, WorderOrder
|
||||
from unilabos.device_comms.modbus_plc.node.modbus import Base as ModbusNodeBase
|
||||
from unilabos.device_comms.modbus_plc.modbus import DeviceType, HoldRegister, Coil, InputRegister, DiscreteInputs, DataType, WorderOrder
|
||||
from unilabos.device_comms.modbus_plc.modbus import Base as ModbusNodeBase
|
||||
from unilabos.device_comms.universal_driver import UniversalDriver
|
||||
from unilabos.utils.log import logger
|
||||
import pandas as pd
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import time
|
||||
from pymodbus.client import ModbusTcpClient
|
||||
from unilabos.device_comms.modbus_plc.node.modbus import Coil, HoldRegister
|
||||
from unilabos.device_comms.modbus_plc.modbus import Coil, HoldRegister
|
||||
from pymodbus.payload import BinaryPayloadDecoder
|
||||
from pymodbus.constants import Endian
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# coding=utf-8
|
||||
from pymodbus.client import ModbusTcpClient
|
||||
from unilabos.device_comms.modbus_plc.node.modbus import Coil
|
||||
from unilabos.device_comms.modbus_plc.modbus import Coil
|
||||
import time
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import time
|
||||
from typing import Callable
|
||||
from unilabos.device_comms.modbus_plc.client import TCPClient, ModbusWorkflow, WorkflowAction, load_csv
|
||||
from unilabos.device_comms.modbus_plc.node.modbus import Base as ModbusNodeBase
|
||||
from unilabos.device_comms.modbus_plc.modbus import Base as ModbusNodeBase
|
||||
|
||||
############ 第一种写法 ##############
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
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
|
||||
@@ -1,45 +0,0 @@
|
||||
<?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>
|
||||
@@ -1,14 +0,0 @@
|
||||
<?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>
|
||||
@@ -1,237 +0,0 @@
|
||||
<?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>
|
||||
@@ -1,73 +0,0 @@
|
||||
###############################################
|
||||
# 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]
|
||||
@@ -1,9 +0,0 @@
|
||||
# Default initial positions for dummy2's ros2_control fake system
|
||||
|
||||
initial_positions:
|
||||
Joint1: 0
|
||||
Joint2: 0
|
||||
Joint3: 0
|
||||
Joint4: 0
|
||||
Joint5: 0
|
||||
Joint6: 0
|
||||
@@ -1,40 +0,0 @@
|
||||
# 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
|
||||
@@ -1,4 +0,0 @@
|
||||
dummy2_arm:
|
||||
kinematics_solver: kdl_kinematics_plugin/KDLKinematicsPlugin
|
||||
kinematics_solver_search_resolution: 0.0050000000000000001
|
||||
kinematics_solver_timeout: 0.5
|
||||
@@ -1,57 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<robot xmlns:xacro="http://www.ros.org/wiki/xacro">
|
||||
<xacro:macro name="dummy2_ros2_control" params="name initial_positions_file">
|
||||
<xacro:property name="initial_positions" value="${load_yaml(initial_positions_file)['initial_positions']}"/>
|
||||
|
||||
<ros2_control name="${name}" type="system">
|
||||
<hardware>
|
||||
<!-- By default, set up controllers for simulation. This won't work on real hardware -->
|
||||
<!-- <plugin>mock_components/GenericSystem</plugin> -->
|
||||
<plugin>dummy2_hardware/Dummy2Hardware</plugin>
|
||||
</hardware>
|
||||
<joint 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="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="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="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="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="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>
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"arm": {
|
||||
"joint_names": [
|
||||
"joint_1",
|
||||
"joint_2",
|
||||
"joint_3",
|
||||
"joint_4",
|
||||
"joint_5",
|
||||
"joint_6"
|
||||
],
|
||||
"base_link_name": "base_link",
|
||||
"end_effector_name": "tool_link"
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
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
|
||||
@@ -1,21 +0,0 @@
|
||||
# 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
|
||||
@@ -1,39 +0,0 @@
|
||||
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 }
|
||||
@@ -1,6 +0,0 @@
|
||||
# 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
|
||||
@@ -1,26 +0,0 @@
|
||||
# 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
|
||||
@@ -1,35 +0,0 @@
|
||||
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
|
||||
@@ -1,37 +0,0 @@
|
||||
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
|
||||
@@ -1,314 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<robot xmlns:xacro="http://wiki.ros.org/xacro" name="dummy2_robot">
|
||||
<xacro:macro name="dummy2_robot" params="mesh_path:='' parent_link:='' station_name:='' device_name:='' fake_dev:='true' x:=0 y:=0 z:=0 rx:=0 ry:=0 r:=0">
|
||||
<!-- Read .yaml files from disk, load content into properties -->
|
||||
<xacro:property name= "joint_limit_parameters" value="${xacro.load_yaml(mesh_path + '/devices/dummy2_robot/joint_limit.yaml')}"/>
|
||||
|
||||
<!-- Extract subsections from yaml dictionaries -->
|
||||
<xacro:property name= "sec_limits" value="${joint_limit_parameters['joint_limits']}"/>
|
||||
|
||||
<!-- robot name parameter -->
|
||||
<xacro:arg name="name" default="dummy2"/>
|
||||
|
||||
<!-- parameters -->
|
||||
<xacro:arg name="tf_prefix" default="${station_name}${device_name}" />
|
||||
<xacro:arg name="joint_limit_params" default="${mesh_path}/devices/dummy2_robot/config/joint_limits.yaml"/>
|
||||
<xacro:arg name="kinematics_params" default="${mesh_path}/devices/dummy2_robot/config/default_kinematics.yaml"/>
|
||||
<xacro:arg name="physical_params" default="${mesh_path}/devices/dummy2_robot/config/physical_parameters.yaml"/>
|
||||
<xacro:arg name="visual_params" default="${mesh_path}/devices/dummy2_robot/config/visual_parameters.yaml"/>
|
||||
<xacro:arg name="transmission_hw_interface" default=""/>
|
||||
<xacro:arg name="safety_limits" default="false"/>
|
||||
<xacro:arg name="safety_pos_margin" default="0.15"/>
|
||||
<xacro:arg name="safety_k_position" default="20"/>
|
||||
|
||||
<!-- CAN2ETH related parameters -->
|
||||
<xacro:arg name="can2eth_ip" default="192.168.8.88" />
|
||||
<xacro:arg name="can2eth_port" default="8080" />
|
||||
|
||||
<!-- JOINTS LIMIT PARAMETERS -->
|
||||
<xacro:property name="limit_joint_1" value="${sec_limits['joint_1']}" />
|
||||
<xacro:property name="limit_joint_2" value="${sec_limits['joint_2']}" />
|
||||
<xacro:property name="limit_joint_3" value="${sec_limits['joint_3']}" />
|
||||
<xacro:property name="limit_joint_4" value="${sec_limits['joint_4']}" />
|
||||
<xacro:property name="limit_joint_5" value="${sec_limits['joint_5']}" />
|
||||
<xacro:property name="limit_joint_6" value="${sec_limits['joint_6']}" />
|
||||
|
||||
<!-- create link fixed to parent -->
|
||||
<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}base_link"/>
|
||||
<axis xyz="0 0 0"/>
|
||||
</joint>
|
||||
|
||||
<!-- base_link -->
|
||||
<link name="${station_name}${device_name}base_link">
|
||||
<inertial>
|
||||
<origin xyz="0 0 0.05" rpy="0 0 0"/>
|
||||
<mass value="5.0"/>
|
||||
<inertia ixx="0.02" ixy="0" ixz="0" iyy="0.02" iyz="0" izz="0.02"/>
|
||||
</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="dummy2_base_material">
|
||||
<color rgba="0.8 0.8 0.8 1.0"/>
|
||||
</material>
|
||||
</visual>
|
||||
<collision>
|
||||
<origin xyz="0 0 0.05" rpy="0 0 0"/>
|
||||
<geometry>
|
||||
<cylinder radius="0.08" length="0.1"/>
|
||||
</geometry>
|
||||
</collision>
|
||||
</link>
|
||||
|
||||
<!-- Joint 1 -->
|
||||
<joint name="${station_name}${device_name}joint_1" type="revolute">
|
||||
<origin xyz="0 0 0.1" rpy="0 0 0"/>
|
||||
<parent link="${station_name}${device_name}base_link"/>
|
||||
<child link="${station_name}${device_name}link_1"/>
|
||||
<axis xyz="0 0 1"/>
|
||||
<limit lower="${limit_joint_1['lower']}" upper="${limit_joint_1['upper']}" effort="${limit_joint_1['effort']}" velocity="${limit_joint_1['velocity']}"/>
|
||||
<dynamics damping="0.0" friction="0.0"/>
|
||||
</joint>
|
||||
|
||||
<link name="${station_name}${device_name}link_1">
|
||||
<inertial>
|
||||
<origin xyz="0 0 0.05" rpy="0 0 0"/>
|
||||
<mass value="3.0"/>
|
||||
<inertia ixx="0.01" ixy="0" ixz="0" iyy="0.01" iyz="0" izz="0.01"/>
|
||||
</inertial>
|
||||
<visual>
|
||||
<origin xyz="0 0 0" 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="dummy2_link_material">
|
||||
<color rgba="0.2 0.2 0.8 1.0"/>
|
||||
</material>
|
||||
</visual>
|
||||
</link>
|
||||
|
||||
<!-- Joint 2 -->
|
||||
<joint name="${station_name}${device_name}joint_2" type="revolute">
|
||||
<origin xyz="0 0 0.15" rpy="0 0 0"/>
|
||||
<parent link="${station_name}${device_name}link_1"/>
|
||||
<child link="${station_name}${device_name}link_2"/>
|
||||
<axis xyz="0 1 0"/>
|
||||
<limit lower="${limit_joint_2['lower']}" upper="${limit_joint_2['upper']}" effort="${limit_joint_2['effort']}" velocity="${limit_joint_2['velocity']}"/>
|
||||
<dynamics damping="0.0" friction="0.0"/>
|
||||
</joint>
|
||||
|
||||
<link name="${station_name}${device_name}link_2">
|
||||
<inertial>
|
||||
<origin xyz="0.1 0 0" rpy="0 0 0"/>
|
||||
<mass value="2.5"/>
|
||||
<inertia ixx="0.008" ixy="0" ixz="0" iyy="0.008" iyz="0" izz="0.008"/>
|
||||
</inertial>
|
||||
<visual>
|
||||
<origin xyz="0 0 0" 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="dummy2_link_material"/>
|
||||
</visual>
|
||||
</link>
|
||||
|
||||
<!-- Joint 3 -->
|
||||
<joint name="${station_name}${device_name}joint_3" type="revolute">
|
||||
<origin xyz="0.2 0 0" rpy="0 0 0"/>
|
||||
<parent link="${station_name}${device_name}link_2"/>
|
||||
<child link="${station_name}${device_name}link_3"/>
|
||||
<axis xyz="0 1 0"/>
|
||||
<limit lower="${limit_joint_3['lower']}" upper="${limit_joint_3['upper']}" effort="${limit_joint_3['effort']}" velocity="${limit_joint_3['velocity']}"/>
|
||||
<dynamics damping="0.0" friction="0.0"/>
|
||||
</joint>
|
||||
|
||||
<link name="${station_name}${device_name}link_3">
|
||||
<inertial>
|
||||
<origin xyz="0.08 0 0" rpy="0 0 0"/>
|
||||
<mass value="2.0"/>
|
||||
<inertia ixx="0.006" ixy="0" ixz="0" iyy="0.006" iyz="0" izz="0.006"/>
|
||||
</inertial>
|
||||
<visual>
|
||||
<origin xyz="0 0 0" 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="dummy2_link_material"/>
|
||||
</visual>
|
||||
</link>
|
||||
|
||||
<!-- Joint 4 -->
|
||||
<joint name="${station_name}${device_name}joint_4" type="revolute">
|
||||
<origin xyz="0.15 0 0" rpy="0 0 0"/>
|
||||
<parent link="${station_name}${device_name}link_3"/>
|
||||
<child link="${station_name}${device_name}link_4"/>
|
||||
<axis xyz="1 0 0"/>
|
||||
<limit lower="${limit_joint_4['lower']}" upper="${limit_joint_4['upper']}" effort="${limit_joint_4['effort']}" velocity="${limit_joint_4['velocity']}"/>
|
||||
<dynamics damping="0.0" friction="0.0"/>
|
||||
</joint>
|
||||
|
||||
<link name="${station_name}${device_name}link_4">
|
||||
<inertial>
|
||||
<origin xyz="0 0 0.05" rpy="0 0 0"/>
|
||||
<mass value="1.5"/>
|
||||
<inertia ixx="0.004" ixy="0" ixz="0" iyy="0.004" iyz="0" izz="0.004"/>
|
||||
</inertial>
|
||||
<visual>
|
||||
<origin xyz="0 0 0" 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="dummy2_link_material"/>
|
||||
</visual>
|
||||
</link>
|
||||
|
||||
<!-- Joint 5 -->
|
||||
<joint name="${station_name}${device_name}joint_5" type="revolute">
|
||||
<origin xyz="0 0 0.1" rpy="0 0 0"/>
|
||||
<parent link="${station_name}${device_name}link_4"/>
|
||||
<child link="${station_name}${device_name}link_5"/>
|
||||
<axis xyz="0 1 0"/>
|
||||
<limit lower="${limit_joint_5['lower']}" upper="${limit_joint_5['upper']}" effort="${limit_joint_5['effort']}" velocity="${limit_joint_5['velocity']}"/>
|
||||
<dynamics damping="0.0" friction="0.0"/>
|
||||
</joint>
|
||||
|
||||
<link name="${station_name}${device_name}link_5">
|
||||
<inertial>
|
||||
<origin xyz="0 0 0.03" rpy="0 0 0"/>
|
||||
<mass value="1.0"/>
|
||||
<inertia ixx="0.002" ixy="0" ixz="0" iyy="0.002" iyz="0" izz="0.002"/>
|
||||
</inertial>
|
||||
<visual>
|
||||
<origin xyz="0 0 0" 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="dummy2_link_material"/>
|
||||
</visual>
|
||||
</link>
|
||||
|
||||
<!-- Joint 6 (end effector) -->
|
||||
<joint name="${station_name}${device_name}joint_6" type="revolute">
|
||||
<origin xyz="0 0 0.06" rpy="0 0 0"/>
|
||||
<parent link="${station_name}${device_name}link_5"/>
|
||||
<child link="${station_name}${device_name}link_6"/>
|
||||
<axis xyz="1 0 0"/>
|
||||
<limit lower="${limit_joint_6['lower']}" upper="${limit_joint_6['upper']}" effort="${limit_joint_6['effort']}" velocity="${limit_joint_6['velocity']}"/>
|
||||
<dynamics damping="0.0" friction="0.0"/>
|
||||
</joint>
|
||||
|
||||
<link name="${station_name}${device_name}link_6">
|
||||
<inertial>
|
||||
<origin xyz="0 0 0.02" rpy="0 0 0"/>
|
||||
<mass value="0.5"/>
|
||||
<inertia ixx="0.001" ixy="0" ixz="0" iyy="0.001" iyz="0" izz="0.001"/>
|
||||
</inertial>
|
||||
<visual>
|
||||
<origin xyz="0 0 0" 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="dummy2_link_material"/>
|
||||
</visual>
|
||||
</link>
|
||||
|
||||
<!-- Tool center point -->
|
||||
<joint name="${station_name}${device_name}tool_joint" type="fixed">
|
||||
<origin xyz="0 0 0.04" rpy="0 0 0"/>
|
||||
<parent link="${station_name}${device_name}link_6"/>
|
||||
<child link="${station_name}${device_name}tool_link"/>
|
||||
</joint>
|
||||
|
||||
<link name="${station_name}${device_name}tool_link"/>
|
||||
|
||||
<!-- Camera link (if needed) -->
|
||||
<joint name="${station_name}${device_name}camera_joint" type="fixed">
|
||||
<origin xyz="0.05 0 0.02" rpy="0 0 0"/>
|
||||
<parent link="${station_name}${device_name}link_6"/>
|
||||
<child link="${station_name}${device_name}camera_link"/>
|
||||
</joint>
|
||||
|
||||
<link name="${station_name}${device_name}camera_link">
|
||||
<visual>
|
||||
<origin xyz="0 0 0" 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="dummy2_camera_material">
|
||||
<color rgba="0.1 0.1 0.1 1.0"/>
|
||||
</material>
|
||||
</visual>
|
||||
</link>
|
||||
|
||||
<!-- ROS2 control (if needed) -->
|
||||
<xacro:unless value="${fake_dev}">
|
||||
<ros2_control name="${station_name}${device_name}ros2_control" type="system">
|
||||
<hardware>
|
||||
<plugin>dummy2_hardware_interface/Dummy2HardwareInterface</plugin>
|
||||
<param name="can2eth_ip">$(arg can2eth_ip)</param>
|
||||
<param name="can2eth_port">$(arg can2eth_port)</param>
|
||||
</hardware>
|
||||
|
||||
<joint name="${station_name}${device_name}joint_1">
|
||||
<command_interface name="position">
|
||||
<param name="min">${limit_joint_1['lower']}</param>
|
||||
<param name="max">${limit_joint_1['upper']}</param>
|
||||
</command_interface>
|
||||
<state_interface name="position"/>
|
||||
<state_interface name="velocity"/>
|
||||
</joint>
|
||||
|
||||
<joint name="${station_name}${device_name}joint_2">
|
||||
<command_interface name="position">
|
||||
<param name="min">${limit_joint_2['lower']}</param>
|
||||
<param name="max">${limit_joint_2['upper']}</param>
|
||||
</command_interface>
|
||||
<state_interface name="position"/>
|
||||
<state_interface name="velocity"/>
|
||||
</joint>
|
||||
|
||||
<joint name="${station_name}${device_name}joint_3">
|
||||
<command_interface name="position">
|
||||
<param name="min">${limit_joint_3['lower']}</param>
|
||||
<param name="max">${limit_joint_3['upper']}</param>
|
||||
</command_interface>
|
||||
<state_interface name="position"/>
|
||||
<state_interface name="velocity"/>
|
||||
</joint>
|
||||
|
||||
<joint name="${station_name}${device_name}joint_4">
|
||||
<command_interface name="position">
|
||||
<param name="min">${limit_joint_4['lower']}</param>
|
||||
<param name="max">${limit_joint_4['upper']}</param>
|
||||
</command_interface>
|
||||
<state_interface name="position"/>
|
||||
<state_interface name="velocity"/>
|
||||
</joint>
|
||||
|
||||
<joint name="${station_name}${device_name}joint_5">
|
||||
<command_interface name="position">
|
||||
<param name="min">${limit_joint_5['lower']}</param>
|
||||
<param name="max">${limit_joint_5['upper']}</param>
|
||||
</command_interface>
|
||||
<state_interface name="position"/>
|
||||
<state_interface name="velocity"/>
|
||||
</joint>
|
||||
|
||||
<joint name="${station_name}${device_name}joint_6">
|
||||
<command_interface name="position">
|
||||
<param name="min">${limit_joint_6['lower']}</param>
|
||||
<param name="max">${limit_joint_6['upper']}</param>
|
||||
</command_interface>
|
||||
<state_interface name="position"/>
|
||||
<state_interface name="velocity"/>
|
||||
</joint>
|
||||
</ros2_control>
|
||||
</xacro:unless>
|
||||
|
||||
</xacro:macro>
|
||||
</robot>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,237 +0,0 @@
|
||||
<?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>
|
||||
6
unilabos/devices/balance/__init__.py
Normal file
6
unilabos/devices/balance/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# Balance devices module
|
||||
|
||||
# Import balance device modules
|
||||
from . import mettler_toledo_xpr
|
||||
|
||||
__all__ = ['mettler_toledo_xpr']
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user