Initial commit
20
docs/Makefile
Normal file
@@ -0,0 +1,20 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line, and also
|
||||
# from the environment for the first two.
|
||||
SPHINXOPTS ?=
|
||||
SPHINXBUILD ?= sphinx-build
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
10
docs/boot_examples/index.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# 启动样例
|
||||
|
||||
本章节提供了几个典型的启动样例,帮助您快速了解和使用系统。每个样例都包含了详细的配置说明、文件解读以及操作步骤,便于您参考和实践。
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
liquid_handler.md
|
||||
organic_synthesis.md
|
||||
```
|
||||
100
docs/boot_examples/liquid_handler.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# 移液站样例
|
||||
|
||||
本样例介绍如何配置和启动移液站设备,并执行基本操作如插入枪头等。
|
||||
|
||||
## 准备工作
|
||||
|
||||
### 设备配置文件
|
||||
|
||||
移液站设备的完整配置可在 `test/experiments/plr_test.json` 文件中找到。该配置文件采用平展结构,通过 `type` 字段区分物料和设备,并通过 `parent` 和 `children` 字段实现层级关系。
|
||||
|
||||
配置文件示例片段:
|
||||
|
||||
```json
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "PLR_STATION",
|
||||
"name": "PLR_LH_TEST",
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "liquid_handler",
|
||||
"config": {},
|
||||
"data": {},
|
||||
"children": [
|
||||
"deck"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "deck",
|
||||
"name": "deck",
|
||||
"type": "container",
|
||||
"class": null,
|
||||
"parent": "PLR_STATION",
|
||||
"children": [
|
||||
"trash",
|
||||
"trash_core96",
|
||||
"teaching_carrier",
|
||||
"tip_rack",
|
||||
"plate"
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
```
|
||||
|
||||
配置文件定义了移液站的组成部分,主要包括:
|
||||
- 移液站本体(LiquidHandler)- 设备类型
|
||||
- 移液站携带物料实例(deck)- 物料类型
|
||||
|
||||
## 启动方法
|
||||
|
||||
### 1. 启动移液站节点
|
||||
|
||||
使用以下命令启动移液站设备:
|
||||
|
||||
```bash
|
||||
unilab -g test/experiments/plr_test.json --app_bridges ""
|
||||
```
|
||||
|
||||
### 2. 执行枪头插入操作
|
||||
|
||||
启动后,您可以使用以下命令执行插入枪头操作:
|
||||
|
||||
```bash
|
||||
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的输出日志。
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
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** - 液体转移
|
||||
|
||||
这些操作可通过ROS2 Action接口进行调用,以实现复杂的移液流程。
|
||||
105
docs/boot_examples/organic_synthesis.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# 有机常量合成样例
|
||||
|
||||
本样例演示如何配置和操作有机常量合成工作站,实现抽真空和充气等基本操作。
|
||||
|
||||
## 准备工作
|
||||
|
||||
### 设备配置文件
|
||||
|
||||
有机常量合成工作站的完整配置可在 `test/experiments/mock_reactor.json` 文件中找到。该配置文件采用平展结构,通过 `type` 字段区分物料和设备,并通过 `parent` 和 `children` 字段实现层级关系。
|
||||
|
||||
配置文件示例片段:
|
||||
|
||||
```json
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "ReactorX",
|
||||
"children": [
|
||||
"reactor",
|
||||
"vacuum_valve",
|
||||
"gas_valve",
|
||||
"vacuum_pump",
|
||||
"gas_source"
|
||||
],
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "workstation"
|
||||
},
|
||||
{
|
||||
"id": "reactor",
|
||||
"parent": "ReactorX",
|
||||
"type": "container"
|
||||
},
|
||||
{
|
||||
"id": "vacuum_valve",
|
||||
"parent": "ReactorX",
|
||||
"type": "device"
|
||||
},
|
||||
{
|
||||
"id": "gas_valve",
|
||||
"parent": "ReactorX",
|
||||
"type": "device"
|
||||
},
|
||||
{
|
||||
"id": "vacuum_pump",
|
||||
"parent": "ReactorX",
|
||||
"type": "device"
|
||||
},
|
||||
{
|
||||
"id": "gas_source",
|
||||
"parent": "ReactorX",
|
||||
"type": "device"
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"source": "reactor",
|
||||
"target": "vacuum_valve"
|
||||
},
|
||||
{
|
||||
"source": "reactor",
|
||||
"target": "gas_valve"
|
||||
},
|
||||
{
|
||||
"source": "vacuum_pump",
|
||||
"target": "vacuum_valve"
|
||||
},
|
||||
{
|
||||
"source": "gas_source",
|
||||
"target": "gas_valve"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
配置文件定义了反应系统的组成部分,主要包括:
|
||||
|
||||
1. **反应工作站 (ReactorX)** - 整个系统的父节点,包含所有子组件
|
||||
2. **反应器 (reactor)** - 实际进行反应的容器
|
||||
3. **真空阀 (vacuum_valve)** - 连接反应器和真空泵
|
||||
4. **气体阀 (gas_valve)** - 连接反应器和气源
|
||||
5. **真空泵 (vacuum_pump)** - 用于抽真空
|
||||
6. **气源 (gas_source)** - 提供充气
|
||||
|
||||
这些组件通过链接关系形成一个完整的气路系统,可以实现抽真空和充气的功能。
|
||||
|
||||
## 启动方法
|
||||
|
||||
### 1. 启动反应器节点
|
||||
|
||||
使用以下命令启动模拟反应器:
|
||||
|
||||
```bash
|
||||
unilab -g test/experiments/mock_reactor.json --app_bridges ""
|
||||
```
|
||||
|
||||
### 2. 执行抽真空和充气操作
|
||||
|
||||
启动后,您可以使用以下命令执行抽真空操作:
|
||||
|
||||
```bash
|
||||
ros2 action send_goal /devices/ReactorX/EvacuateAndRefillProtocol unilabos_msgs/action/EvacuateAndRefill "{vessel: reactor, gas: N2, repeats: 2}"
|
||||
```
|
||||
|
||||
此命令会通过ros通信触发工作站执行抽真空和充气的协议操作,与此同时,您可以通过 http://localhost:8002/status 在`主机节点信息`-`设备状态`查看该操作对设备开关的实时效果。
|
||||
45
docs/concepts/01-communication-instruction.md
Normal file
@@ -0,0 +1,45 @@
|
||||
(instructions)=
|
||||
# 设备抽象、指令集与通信中间件
|
||||
|
||||
Uni-Lab 操作系统的目的是将不同类型和厂家的实验仪器进行抽象统一,对应用层提供服务。因此,理清实验室设备之间的业务逻辑至关重要。
|
||||
|
||||
## 设备间通信模式
|
||||
|
||||
### **[话题(一对多发送,一对多订阅)](https://book.guyuehome.com/ROS2/2.%E6%A0%B8%E5%BF%83%E6%A6%82%E5%BF%B5/2.4_%E8%AF%9D%E9%A2%98/)**
|
||||
|
||||
典型的话题通信有:
|
||||
|
||||
* 传感器连续发送设备状态和数据;
|
||||
* 连续时间控制器发送控制指令,如控温、连续称量、机械臂轨迹跟随、视觉识别操作等
|
||||
|
||||

|
||||
|
||||
### **[服务(短时请求与响应)](https://book.guyuehome.com/ROS2/2.%E6%A0%B8%E5%BF%83%E6%A6%82%E5%BF%B5/2.5_%E6%9C%8D%E5%8A%A1/)**
|
||||
|
||||
典型的服务通信有:
|
||||
|
||||
* 查/改全局参数如物料、设备
|
||||
* 使用其他通信接口发送/接收数据
|
||||
|
||||

|
||||
|
||||
### **[动作(长时任务启动,随后连续收到反馈值,直到达到目标)](https://book.guyuehome.com/ROS2/2.%E6%A0%B8%E5%BF%83%E6%A6%82%E5%BF%B5/2.7_%E5%8A%A8%E4%BD%9C/)**
|
||||
|
||||
动作机制主要用于处理运行时长较长的单点任务或任务组合,如:
|
||||
|
||||
* 执行工作流
|
||||
* 执行工作流的子动作
|
||||
|
||||

|
||||
|
||||
## 通信指令集
|
||||
|
||||
Uni-Lab 目前使用 ROS2 作为通信中间件,因此大量使用其标准消息作为话题、服务、动作。新增指令位于仓库中的 `unilabos_msgs` ,各类实验动作指令集分类整理于 {ref}`actions`
|
||||
|
||||
## 通信中间件层
|
||||
|
||||
通信中间件层的一个重要设计思想是:将业务逻辑开发,与实际部署中的通信和运行解耦。开发者在实现具体业务逻辑时,可以不用关心最终运行时的 **部署方式** 、 **通信方式** 。当用户开发完成后,再根据实际情况决定部署、通信方案。
|
||||
|
||||
* 对于 **“流动化学实验室”和“桌面机器人”** 来说,一台电脑通过串口控制所有设备足够。**此时在这台电脑启动 Uni-Lab 作为 Server 即可。**
|
||||
* 对于 **“移动机器人”大型实验室** ,典型场景是,一个实验室由多台不同位置的工作站组成,每台大型设备有一台工控机,通过串口再控制子设备。同时有 AGV/机械臂 负责转运。**此时,在每台工控机启动 Uni-Lab,完成通信中间件层的包装之后,只要处于同一局域网下,他们将能自动互相发现并组成分布式的“Uni-Lab-Edge Server”。**
|
||||
* 通信中间件层的分布式机制,使得 Node 之间做好了隔离,一台设备故障时只需重启单个 Node。很像微服务、微内核的设计理念。
|
||||
79
docs/concepts/02-topology-and-chemputer-compile.md
Normal file
@@ -0,0 +1,79 @@
|
||||
(graph)=
|
||||
# 实验室组态图
|
||||
|
||||
组态(configuration)图是指在自动化领域中,用来描述和展示控制系统中各个组件之间关系的图形化表示方法。
|
||||
它是一个系统的框架图,通过图形符号和连接线,将各个组件(如传感器、执行器、控制器等)以及它们之间的关系进行可视化展示。
|
||||
|
||||
Uni-Lab 的组态图当前支持 node-link json 和 graphml 格式,其中包含4类重要信息:
|
||||
|
||||
* 单个设备/物料配置,即图中节点的参数;
|
||||
* 父子关系,如一台工作站包含它的多个子设备、放置着多个物料耗材;
|
||||
* 物理连接关系,如流体管路连接、AGV/机械臂/直线模组转运连接。
|
||||
* 通信转接关系,如多个 IO 设备通过 IO 板卡或 PLC 转为 Modbus;串口转网口等
|
||||
* 控制逻辑关系,如某个输出量被某个输入量 PID 控制
|
||||
|
||||
## 父子关系、物质流与"编译"操作
|
||||
|
||||
在计算机操作系统下,软件操作数据和文件。在实验操作系统下,实验“软件”利用仪器“硬件”操作物质。实验人员能理解的操作,最终都是对物质的处理。将实验步骤,转化为硬件指令,这个操作我们可以类比为“编译”。
|
||||
|
||||
对用户来说,“直接操作设备执行单个指令”不是个真实需求,真正的需求是**“执行对实验有意义的单个完整动作”——加入某种液体多少量;萃取分液;洗涤仪器等等。就像实验步骤文字书写的那样。**
|
||||
|
||||
而这些对实验有意义的单个完整动作,**一般需要多个设备的协同**,还依赖于他们的**物理连接关系(管道相连;机械臂可转运)**。
|
||||
于是 Uni-Lab 实现了抽象的“工作站”,即注册表中的 `workstation` 设备(`ProtocolNode`类)来处理编译、规划操作。以泵骨架组成的自动有机实验室为例,设备管道连接关系如下:
|
||||
|
||||

|
||||
|
||||
接收“移液”动作,编译为一系列泵指令和阀指令
|
||||
|
||||
```text
|
||||
Goal received: {
|
||||
'from_vessel': 'flask_acetone',
|
||||
'to_vessel': 'reactor',
|
||||
'volume': 2000.0,
|
||||
'flowrate': 100.0
|
||||
}, running steps:
|
||||
```
|
||||
|
||||
```JSON
|
||||
[
|
||||
{
|
||||
"device_id": "pump_reagents",
|
||||
"action_name": "set_valve_position",
|
||||
"action_kwargs": {"command": "3"}
|
||||
},
|
||||
{
|
||||
"device_id": "pump_reagents",
|
||||
"action_name": "set_position",
|
||||
"action_kwargs": {
|
||||
"position": 2000.0,
|
||||
"max_velocity": 100.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"device_id": "pump_reagents",
|
||||
"action_name": "set_valve_position",
|
||||
"action_kwargs": {"command": '5'}
|
||||
},
|
||||
{
|
||||
"device_id": "pump_reagents",
|
||||
"action_name": "set_position",
|
||||
"action_kwargs": {
|
||||
"position": 0.0,
|
||||
"max_velocity": 100.0
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
若想开发新的“编译”/“规划”功能,在 `unilabos/compilers` 实现一个新函数即可。详情请见 [添加新实验操作(Protocol)](../developer_guide/add_protocol.md)
|
||||
|
||||
## 通信转接关系
|
||||
|
||||
Uni-Lab 秉持着**通信逻辑**与**业务逻辑**分离的设计理念,以追求实验设备、通信设备最大的代码复用性。
|
||||
|
||||
如对 IO 板卡8路 IO 控制的8个电磁阀,经过设备抽象层后,仅有逻辑意义上分立的8个电磁阀。在组态图中,他们通过代表通信关系的边(edge)与 IO 板卡设备相连,边的属性包含 IO 地址。
|
||||
在实际使用中,对抽象的电磁阀发送开关动作请求,将通过 Uni-Lab 设备抽象&通信中间件转化为对 IO 板卡发送 IO 读写指令。因此一定意义上,组态图的通信转接部分代表了简化的电气接线图。
|
||||
|
||||
代码架构上,实验设备位于 Uni-Lab 仓库/注册表的 `devices` 目录,通信设备位于 Uni-Lab 仓库/注册表的 `device_comms` 目录。
|
||||
|
||||

|
||||
BIN
docs/concepts/image/01-communication-instruction/action.gif
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 304 KiB |
BIN
docs/concepts/image/01-communication-instruction/service.gif
Normal file
|
After Width: | Height: | Size: 371 KiB |
BIN
docs/concepts/image/01-communication-instruction/topic.gif
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 586 KiB |
BIN
docs/concepts/image/overview/Uni-Lab-layers.png
Normal file
|
After Width: | Height: | Size: 206 KiB |
BIN
docs/concepts/image/overview/Uni-Lab-whiteboard.png
Normal file
|
After Width: | Height: | Size: 612 KiB |
3
docs/concepts/overview.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Uni-Lab 操作系统总览
|
||||
|
||||

|
||||
205
docs/conf.py
Normal file
@@ -0,0 +1,205 @@
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
# Sphinx 文档生成器的配置文件
|
||||
# For the full list of built-in configuration values, see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# 将项目的根目录添加到 sys.path 中,以便 Sphinx 能够找到 unilabos 包
|
||||
sys.path.insert(0, os.path.abspath(".."))
|
||||
|
||||
project = "Uni-Lab"
|
||||
copyright = "2025, Uni-Lab Community, DP Technology & Peking University"
|
||||
author = "Uni-Lab Community, DP Technology & Peking University"
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||
|
||||
extensions = [
|
||||
"myst_parser",
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.napoleon", # 如果您使用 Google 或 NumPy 风格的 docstrings
|
||||
"sphinx_rtd_theme"
|
||||
]
|
||||
|
||||
source_suffix = {
|
||||
".rst": "restructuredtext",
|
||||
".txt": "markdown",
|
||||
".md": "markdown",
|
||||
}
|
||||
|
||||
myst_enable_extensions = [
|
||||
"colon_fence",
|
||||
"deflist",
|
||||
"dollarmath",
|
||||
"html_image",
|
||||
"replacements",
|
||||
"smartquotes",
|
||||
"substitution",
|
||||
]
|
||||
|
||||
templates_path = ["_templates"]
|
||||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||
|
||||
language = "zh"
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||
|
||||
# 设置 HTML 主题为 sphinx-book-theme
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
|
||||
# sphinx-book-theme 主题选项
|
||||
html_theme_options = {
|
||||
"repository_url": "https://github.com/用户名/Uni-Lab",
|
||||
"use_repository_button": True,
|
||||
"use_issues_button": True,
|
||||
"use_edit_page_button": True,
|
||||
"use_download_button": True,
|
||||
"path_to_docs": "docs",
|
||||
"show_navbar_depth": 2,
|
||||
"show_toc_level": 2,
|
||||
"home_page_in_toc": True,
|
||||
"logo_only": False,
|
||||
}
|
||||
|
||||
# 设置 HTML 文档的静态文件路径
|
||||
html_static_path = ["_static"] # 如果有自定义 CSS,可以放在 _static 目录中
|
||||
|
||||
section_titles = {
|
||||
"Simple": "## 简单单变量动作函数",
|
||||
"Organic": """## 常量有机化学操作
|
||||
|
||||
Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab.io/chemputer/xdl/standard/full_steps_specification.html#),包含有机合成实验中常见的操作,如加热、搅拌、冷却等。
|
||||
""",
|
||||
"Bio": """## 移液工作站及相关生物自动化设备操作
|
||||
|
||||
Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.org/user_guide/index.html),包含生物实验中常见的操作,如移液、混匀、离心等。
|
||||
""",
|
||||
"MobileRobot": "## 多工作站及小车运行、物料转移",
|
||||
"Robot": """## 机械臂、夹爪等机器人设备
|
||||
|
||||
Uni-Lab 机械臂、机器人、夹爪和导航指令集沿用 ROS2 的 `control_msgs` 和 `nav2_msgs`:
|
||||
""",
|
||||
}
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_conda_share_dir(package_name=None):
|
||||
"""获取 Conda 环境的 share 目录路径
|
||||
|
||||
:param package_name: 可选参数,指定具体包的 share 子目录
|
||||
:return: Path 对象或 None
|
||||
"""
|
||||
# 获取当前 Conda 环境根目录
|
||||
conda_prefix = os.getenv("CONDA_PREFIX")
|
||||
if not conda_prefix:
|
||||
raise EnvironmentError("未检测到激活的 Conda 环境")
|
||||
|
||||
# 构建基础 share 目录路径
|
||||
share_dir = Path(conda_prefix) / "share"
|
||||
|
||||
# 如果指定了包名,追加包子目录
|
||||
if package_name:
|
||||
share_dir = share_dir / package_name
|
||||
|
||||
# 验证路径是否存在
|
||||
if not share_dir.exists():
|
||||
print(f"警告: 路径 {share_dir} 不存在")
|
||||
return None
|
||||
|
||||
return share_dir
|
||||
|
||||
|
||||
def generate_action_includes(app):
|
||||
src_dir = Path(app.srcdir)
|
||||
print(f"Generating action includes for {src_dir}")
|
||||
action_dir = src_dir.parent / "unilabos_msgs" / "action" # 修改为你的实际路径
|
||||
output_file = src_dir / "developer_guide" / "action_includes.md"
|
||||
|
||||
# 确保输出目录存在
|
||||
output_file.parent.mkdir(exist_ok=True)
|
||||
|
||||
# 初始化各部分内容
|
||||
sections = {}
|
||||
|
||||
# 仅处理本地消息文件
|
||||
if action_dir.exists():
|
||||
for action_file in sorted(action_dir.glob("*.action")):
|
||||
# 获取相对路径
|
||||
rel_path = f"../../unilabos_msgs/action/{action_file.name}"
|
||||
# 读取首行注释
|
||||
try:
|
||||
with open(action_file, "r", encoding="utf-8") as af:
|
||||
first_line = af.readline().strip()
|
||||
# 提取注释内容(去除#和空格)
|
||||
section = first_line.lstrip("#").strip()
|
||||
|
||||
text = f"""
|
||||
### `{action_file.stem}`
|
||||
|
||||
```{{literalinclude}} {rel_path}
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
"""
|
||||
|
||||
if sections.get(section) is None:
|
||||
sections[section] = text
|
||||
else:
|
||||
sections[section] += text
|
||||
except Exception as e:
|
||||
print(f"处理文件 {action_file} 时出错: {e}")
|
||||
else:
|
||||
print(f"警告: 动作消息目录 {action_dir} 不存在")
|
||||
|
||||
ros_action_dirs = []
|
||||
control_msgs_dir = get_conda_share_dir("control_msgs")
|
||||
nav2_msgs_dir = get_conda_share_dir("nav2_msgs")
|
||||
|
||||
if control_msgs_dir is not None:
|
||||
ros_action_dirs.append(control_msgs_dir / "action")
|
||||
if nav2_msgs_dir is not None:
|
||||
ros_action_dirs.append(nav2_msgs_dir / "action")
|
||||
|
||||
for action_dir in ros_action_dirs:
|
||||
for action_file in sorted(action_dir.glob("*.action")):
|
||||
# 获取相对路径
|
||||
rel_path = f"{action_file.absolute()}"
|
||||
# 读取首行注释
|
||||
with open(action_file, "r", encoding="utf-8") as af:
|
||||
# 提取注释内容(去除#和空格)
|
||||
section = "Robot"
|
||||
|
||||
text = f"""### `{action_file.stem}`
|
||||
|
||||
```yaml
|
||||
{open(rel_path, 'r').read()}
|
||||
```
|
||||
|
||||
----
|
||||
"""
|
||||
if sections.get(section) is None:
|
||||
sections[section] = text
|
||||
else:
|
||||
sections[section] += text
|
||||
|
||||
# 写入内容到输出文件
|
||||
with open(output_file, "w", encoding="utf-8") as f:
|
||||
# 按 Section 生成总文档
|
||||
for section, title in section_titles.items():
|
||||
content = sections.get(section, "")
|
||||
if content: # 只有有内容时才写入标题和内容
|
||||
f.write(f"{title}\n\n")
|
||||
f.write(content)
|
||||
|
||||
|
||||
def setup(app):
|
||||
app.connect("builder-inited", generate_action_includes)
|
||||
577
docs/developer_guide/action_includes.md
Normal file
@@ -0,0 +1,577 @@
|
||||
## 简单单变量动作函数
|
||||
|
||||
|
||||
### `SendCmd`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/SendCmd.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
## 常量有机化学操作
|
||||
|
||||
Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab.io/chemputer/xdl/standard/full_steps_specification.html#),包含有机合成实验中常见的操作,如加热、搅拌、冷却等。
|
||||
|
||||
|
||||
|
||||
### `Clean`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/Clean.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### `EvacuateAndRefill`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/EvacuateAndRefill.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### `Evaporate`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/Evaporate.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### `HeatChill`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/HeatChill.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### `HeatChillStart`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/HeatChillStart.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### `HeatChillStop`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/HeatChillStop.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### `PumpTransfer`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/PumpTransfer.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### `Separate`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/Separate.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### `Stir`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/Stir.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
## 移液工作站及相关生物自动化设备操作
|
||||
|
||||
Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.org/user_guide/index.html),包含生物实验中常见的操作,如移液、混匀、离心等。
|
||||
|
||||
|
||||
|
||||
### `LiquidHandlerAspirate`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerAspirate.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### `LiquidHandlerDiscardTips`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerDiscardTips.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### `LiquidHandlerDispense`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerDispense.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### `LiquidHandlerDropTips`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerDropTips.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### `LiquidHandlerDropTips96`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerDropTips96.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### `LiquidHandlerMoveLid`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerMoveLid.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### `LiquidHandlerMovePlate`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerMovePlate.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### `LiquidHandlerMoveResource`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerMoveResource.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### `LiquidHandlerPickUpTips`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerPickUpTips.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### `LiquidHandlerPickUpTips96`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerPickUpTips96.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### `LiquidHandlerReturnTips`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerReturnTips.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### `LiquidHandlerReturnTips96`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerReturnTips96.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### `LiquidHandlerStamp`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerStamp.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### `LiquidHandlerTransfer`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerTransfer.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
## 多工作站及小车运行、物料转移
|
||||
|
||||
|
||||
### `AGVTransfer`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/AGVTransfer.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
### `WorkStationRun`
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/WorkStationRun.action
|
||||
:language: yaml
|
||||
```
|
||||
|
||||
----
|
||||
## 机械臂、夹爪等机器人设备
|
||||
|
||||
Uni-Lab 机械臂、机器人、夹爪和导航指令集沿用 ROS2 的 `control_msgs` 和 `nav2_msgs`:
|
||||
|
||||
|
||||
### `FollowJointTrajectory`
|
||||
|
||||
```yaml
|
||||
# The trajectory for all revolute, continuous or prismatic joints
|
||||
trajectory_msgs/JointTrajectory trajectory
|
||||
# The trajectory for all planar or floating joints (i.e. individual joints with more than one DOF)
|
||||
trajectory_msgs/MultiDOFJointTrajectory multi_dof_trajectory
|
||||
|
||||
# Tolerances for the trajectory. If the measured joint values fall
|
||||
# outside the tolerances the trajectory goal is aborted. Any
|
||||
# tolerances that are not specified (by being omitted or set to 0) are
|
||||
# set to the defaults for the action server (often taken from the
|
||||
# parameter server).
|
||||
|
||||
# Tolerances applied to the joints as the trajectory is executed. If
|
||||
# violated, the goal aborts with error_code set to
|
||||
# PATH_TOLERANCE_VIOLATED.
|
||||
JointTolerance[] path_tolerance
|
||||
JointComponentTolerance[] component_path_tolerance
|
||||
|
||||
# To report success, the joints must be within goal_tolerance of the
|
||||
# final trajectory value. The goal must be achieved by time the
|
||||
# trajectory ends plus goal_time_tolerance. (goal_time_tolerance
|
||||
# allows some leeway in time, so that the trajectory goal can still
|
||||
# succeed even if the joints reach the goal some time after the
|
||||
# precise end time of the trajectory).
|
||||
#
|
||||
# If the joints are not within goal_tolerance after "trajectory finish
|
||||
# time" + goal_time_tolerance, the goal aborts with error_code set to
|
||||
# GOAL_TOLERANCE_VIOLATED
|
||||
JointTolerance[] goal_tolerance
|
||||
JointComponentTolerance[] component_goal_tolerance
|
||||
builtin_interfaces/Duration goal_time_tolerance
|
||||
|
||||
---
|
||||
int32 error_code
|
||||
int32 SUCCESSFUL = 0
|
||||
int32 INVALID_GOAL = -1
|
||||
int32 INVALID_JOINTS = -2
|
||||
int32 OLD_HEADER_TIMESTAMP = -3
|
||||
int32 PATH_TOLERANCE_VIOLATED = -4
|
||||
int32 GOAL_TOLERANCE_VIOLATED = -5
|
||||
|
||||
# Human readable description of the error code. Contains complementary
|
||||
# information that is especially useful when execution fails, for instance:
|
||||
# - INVALID_GOAL: The reason for the invalid goal (e.g., the requested
|
||||
# trajectory is in the past).
|
||||
# - INVALID_JOINTS: The mismatch between the expected controller joints
|
||||
# and those provided in the goal.
|
||||
# - PATH_TOLERANCE_VIOLATED and GOAL_TOLERANCE_VIOLATED: Which joint
|
||||
# violated which tolerance, and by how much.
|
||||
string error_string
|
||||
|
||||
---
|
||||
std_msgs/Header header
|
||||
string[] joint_names
|
||||
trajectory_msgs/JointTrajectoryPoint desired
|
||||
trajectory_msgs/JointTrajectoryPoint actual
|
||||
trajectory_msgs/JointTrajectoryPoint error
|
||||
|
||||
string[] multi_dof_joint_names
|
||||
trajectory_msgs/MultiDOFJointTrajectoryPoint multi_dof_desired
|
||||
trajectory_msgs/MultiDOFJointTrajectoryPoint multi_dof_actual
|
||||
trajectory_msgs/MultiDOFJointTrajectoryPoint multi_dof_error
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
### `GripperCommand`
|
||||
|
||||
```yaml
|
||||
GripperCommand command
|
||||
---
|
||||
float64 position # The current gripper gap size (in meters)
|
||||
float64 effort # The current effort exerted (in Newtons)
|
||||
bool stalled # True iff the gripper is exerting max effort and not moving
|
||||
bool reached_goal # True iff the gripper position has reached the commanded setpoint
|
||||
---
|
||||
float64 position # The current gripper gap size (in meters)
|
||||
float64 effort # The current effort exerted (in Newtons)
|
||||
bool stalled # True iff the gripper is exerting max effort and not moving
|
||||
bool reached_goal # True iff the gripper position has reached the commanded setpoint
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
### `JointTrajectory`
|
||||
|
||||
```yaml
|
||||
trajectory_msgs/JointTrajectory trajectory
|
||||
---
|
||||
---
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
### `PointHead`
|
||||
|
||||
```yaml
|
||||
geometry_msgs/PointStamped target
|
||||
geometry_msgs/Vector3 pointing_axis
|
||||
string pointing_frame
|
||||
builtin_interfaces/Duration min_duration
|
||||
float64 max_velocity
|
||||
---
|
||||
---
|
||||
float64 pointing_angle_error
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
### `SingleJointPosition`
|
||||
|
||||
```yaml
|
||||
float64 position
|
||||
builtin_interfaces/Duration min_duration
|
||||
float64 max_velocity
|
||||
---
|
||||
---
|
||||
std_msgs/Header header
|
||||
float64 position
|
||||
float64 velocity
|
||||
float64 error
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
### `AssistedTeleop`
|
||||
|
||||
```yaml
|
||||
#goal definition
|
||||
builtin_interfaces/Duration time_allowance
|
||||
---
|
||||
#result definition
|
||||
builtin_interfaces/Duration total_elapsed_time
|
||||
---
|
||||
#feedback
|
||||
builtin_interfaces/Duration current_teleop_duration
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
### `BackUp`
|
||||
|
||||
```yaml
|
||||
#goal definition
|
||||
geometry_msgs/Point target
|
||||
float32 speed
|
||||
builtin_interfaces/Duration time_allowance
|
||||
---
|
||||
#result definition
|
||||
builtin_interfaces/Duration total_elapsed_time
|
||||
---
|
||||
#feedback definition
|
||||
float32 distance_traveled
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
### `ComputePathThroughPoses`
|
||||
|
||||
```yaml
|
||||
#goal definition
|
||||
geometry_msgs/PoseStamped[] goals
|
||||
geometry_msgs/PoseStamped start
|
||||
string planner_id
|
||||
bool use_start # If false, use current robot pose as path start, if true, use start above instead
|
||||
---
|
||||
#result definition
|
||||
nav_msgs/Path path
|
||||
builtin_interfaces/Duration planning_time
|
||||
---
|
||||
#feedback definition
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
### `ComputePathToPose`
|
||||
|
||||
```yaml
|
||||
#goal definition
|
||||
geometry_msgs/PoseStamped goal
|
||||
geometry_msgs/PoseStamped start
|
||||
string planner_id
|
||||
bool use_start # If false, use current robot pose as path start, if true, use start above instead
|
||||
---
|
||||
#result definition
|
||||
nav_msgs/Path path
|
||||
builtin_interfaces/Duration planning_time
|
||||
---
|
||||
#feedback definition
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
### `DriveOnHeading`
|
||||
|
||||
```yaml
|
||||
#goal definition
|
||||
geometry_msgs/Point target
|
||||
float32 speed
|
||||
builtin_interfaces/Duration time_allowance
|
||||
---
|
||||
#result definition
|
||||
builtin_interfaces/Duration total_elapsed_time
|
||||
---
|
||||
#feedback definition
|
||||
float32 distance_traveled
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
### `DummyBehavior`
|
||||
|
||||
```yaml
|
||||
#goal definition
|
||||
std_msgs/String command
|
||||
---
|
||||
#result definition
|
||||
builtin_interfaces/Duration total_elapsed_time
|
||||
---
|
||||
#feedback definition
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
### `FollowPath`
|
||||
|
||||
```yaml
|
||||
#goal definition
|
||||
nav_msgs/Path path
|
||||
string controller_id
|
||||
string goal_checker_id
|
||||
---
|
||||
#result definition
|
||||
std_msgs/Empty result
|
||||
---
|
||||
#feedback definition
|
||||
float32 distance_to_goal
|
||||
float32 speed
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
### `FollowWaypoints`
|
||||
|
||||
```yaml
|
||||
#goal definition
|
||||
geometry_msgs/PoseStamped[] poses
|
||||
---
|
||||
#result definition
|
||||
int32[] missed_waypoints
|
||||
---
|
||||
#feedback definition
|
||||
uint32 current_waypoint
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
### `NavigateThroughPoses`
|
||||
|
||||
```yaml
|
||||
#goal definition
|
||||
geometry_msgs/PoseStamped[] poses
|
||||
string behavior_tree
|
||||
---
|
||||
#result definition
|
||||
std_msgs/Empty result
|
||||
---
|
||||
#feedback definition
|
||||
geometry_msgs/PoseStamped current_pose
|
||||
builtin_interfaces/Duration navigation_time
|
||||
builtin_interfaces/Duration estimated_time_remaining
|
||||
int16 number_of_recoveries
|
||||
float32 distance_remaining
|
||||
int16 number_of_poses_remaining
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
### `NavigateToPose`
|
||||
|
||||
```yaml
|
||||
#goal definition
|
||||
geometry_msgs/PoseStamped pose
|
||||
string behavior_tree
|
||||
---
|
||||
#result definition
|
||||
std_msgs/Empty result
|
||||
---
|
||||
#feedback definition
|
||||
geometry_msgs/PoseStamped current_pose
|
||||
builtin_interfaces/Duration navigation_time
|
||||
builtin_interfaces/Duration estimated_time_remaining
|
||||
int16 number_of_recoveries
|
||||
float32 distance_remaining
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
### `SmoothPath`
|
||||
|
||||
```yaml
|
||||
#goal definition
|
||||
nav_msgs/Path path
|
||||
string smoother_id
|
||||
builtin_interfaces/Duration max_smoothing_duration
|
||||
bool check_for_collisions
|
||||
---
|
||||
#result definition
|
||||
nav_msgs/Path path
|
||||
builtin_interfaces/Duration smoothing_duration
|
||||
bool was_completed
|
||||
---
|
||||
#feedback definition
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
### `Spin`
|
||||
|
||||
```yaml
|
||||
#goal definition
|
||||
float32 target_yaw
|
||||
builtin_interfaces/Duration time_allowance
|
||||
---
|
||||
#result definition
|
||||
builtin_interfaces/Duration total_elapsed_time
|
||||
---
|
||||
#feedback definition
|
||||
float32 angular_distance_traveled
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
### `Wait`
|
||||
|
||||
```yaml
|
||||
#goal definition
|
||||
builtin_interfaces/Duration time
|
||||
---
|
||||
#result definition
|
||||
builtin_interfaces/Duration total_elapsed_time
|
||||
---
|
||||
#feedback definition
|
||||
builtin_interfaces/Duration time_left
|
||||
|
||||
```
|
||||
|
||||
----
|
||||
7
docs/developer_guide/actions.md
Normal file
@@ -0,0 +1,7 @@
|
||||
(actions)=
|
||||
# Uni-Lab 动作指令集
|
||||
|
||||
Uni-Lab 当前动作指令集设计时,主要考虑兼容领域历史开源工具。目前包括以下场景:
|
||||
|
||||
```{include} action_includes.md
|
||||
```
|
||||
37
docs/developer_guide/add_action.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# 添加新动作指令(Action)
|
||||
|
||||
1. 在 `unilabos_msgs/action` 中新建实验操作名和参数列表,如 `MyDeviceCmd.action`。一个 Action 定义由三个部分组成,分别是目标(Goal)、结果(Result)和反馈(Feedback),之间使用 `---` 分隔:
|
||||
|
||||
```action
|
||||
# 目标(Goal)
|
||||
string command
|
||||
---
|
||||
# 结果(Result)
|
||||
bool success
|
||||
---
|
||||
# 反馈(Feedback)
|
||||
```
|
||||
|
||||
2. 在 `unilabos_msgs/CMakeLists.txt` 中添加新定义的 action
|
||||
|
||||
```cmake
|
||||
add_action_files(
|
||||
FILES
|
||||
MyDeviceCmd.action
|
||||
)
|
||||
```
|
||||
|
||||
3. 因为在指令集中新建了指令,因此调试时需要编译,并在终端环境中加载临时路径:
|
||||
|
||||
```bash
|
||||
cd unilabos_msgs
|
||||
colcon build
|
||||
source ./install/local_setup.sh
|
||||
cd ..
|
||||
```
|
||||
|
||||
调试成功后,发起 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
|
||||
```
|
||||
200
docs/developer_guide/add_device.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# 添加新设备
|
||||
|
||||
在 Uni-Lab 中,设备(Device)是实验操作的基础单元。Uni-Lab 使用**注册表机制**来兼容管理种类繁多的设备驱动程序。回顾 {ref}`instructions` 中的概念,抽象的设备对外拥有【话题】【服务】【动作】三种通信机制,因此将设备添加进 Uni-Lab,实际上是将设备驱动中的三种机制映射到 Uni-Lab 标准指令集上。
|
||||
|
||||
能被 Uni-Lab 添加的驱动程序类型有以下种类:
|
||||
|
||||
1. Python Class,如
|
||||
|
||||
```python
|
||||
class MockGripper:
|
||||
def __init__(self):
|
||||
self._position: float = 0.0
|
||||
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)
|
||||
self._torque = torque / (20 - i)
|
||||
self._velocity = velocity
|
||||
time.sleep(move_time / 20)
|
||||
self._torque = torque
|
||||
self._status = "Idle"
|
||||
```
|
||||
|
||||
Python 类设备驱动在完成注册表后可以直接在 Uni-Lab 使用。
|
||||
|
||||
2. C# Class,如
|
||||
|
||||
```csharp
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
public class MockGripper
|
||||
{
|
||||
// 会被自动识别的设备属性,接入 Uni-Lab 时会定时对外广播
|
||||
public double position { get; private set; } = 0.0;
|
||||
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)
|
||||
{
|
||||
status = "Running";
|
||||
double currentPos = Position;
|
||||
if (Velocity == 0.0)
|
||||
{
|
||||
velocity = Velocity;
|
||||
}
|
||||
double moveTime = Math.Abs(Position - currentPos) / velocity;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
position = currentPos + (Position - currentPos) / 20 * (i + 1);
|
||||
torque = Torque / (20 - i);
|
||||
velocity = Velocity;
|
||||
await Task.Delay((int)(moveTime * 1000 / 20)); // Convert seconds to milliseconds
|
||||
}
|
||||
torque = Torque;
|
||||
status = "Idle";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
C# 驱动设备在完成注册表后,需要调用 Uni-Lab C# 编译后才能使用,但只需一次。
|
||||
|
||||
## 注册表文件位置
|
||||
|
||||
Uni-Lab 启动时会自动读取默认注册表路径 `unilabos/registry/devices` 下的所有注册设备。您也可以任意维护自己的注册表路径,只需要在 Uni-Lab 启动时使用 `--registry` 参数将路径添加即可。
|
||||
|
||||
在 `<path-to-registry>/devices` 中新建一个 yaml 文件,即可开始撰写。您可以将多个设备写到同一个 yaml 文件中。
|
||||
|
||||
## 注册表的结构
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
4. 定义设备的定时发布属性。注意,对于 Python Class 来说,PROP 是 class 的 `property`,或满足能被 `getattr(cls, PROP)` 或 `cls.get_PROP` 读取到的属性值的对象。
|
||||
|
||||
```yaml
|
||||
status_types:
|
||||
PROP: TYPE
|
||||
```
|
||||
5. 定义设备支持的动作
|
||||
添加设备支持的动作及其目标、反馈和结果:
|
||||
|
||||
```yaml
|
||||
action_value_mappings:
|
||||
set_speed:
|
||||
type: SendCmd
|
||||
goal:
|
||||
command: speed
|
||||
feedback: {}
|
||||
result:
|
||||
success: success
|
||||
```
|
||||
|
||||
在 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
|
||||
```
|
||||
50
docs/developer_guide/add_protocol.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# 添加新实验操作(Protocol)
|
||||
|
||||
在 `Uni-Lab` 中,实验操作(Protocol)指的是**对实验有意义的单个完整动作**——加入某种液体多少量;萃取分液;洗涤仪器;机械+末端执行器等等,就像实验步骤文字书写的那样。
|
||||
|
||||
而这些对实验有意义的单个完整动作,**一般需要多个设备的协同**,或者同一设备连续动作,还依赖于他们的**物理连接关系(管道相连;机械臂可转运)**。`Protocol` 根据实验操作目标和设备物理连接关系,通过 `unilabos/compile` 中的“编译”过程产生硬件可执行的机器指令,并依次执行。
|
||||
|
||||
开发一个 `Protocol` 一般共需要修改6个文件:
|
||||
|
||||
1. 在 `unilabos_msgs/action` 中新建实验操作名和参数列表,如 `PumpTransfer.action`。一个 Action 定义由三个部分组成,分别是目标(Goal)、结果(Result)和反馈(Feedback),之间使用 `---` 分隔:
|
||||
|
||||
```{literalinclude} ../../unilabos_msgs/action/PumpTransfer.action
|
||||
```
|
||||
|
||||
2. 在 `unilabos_msgs/CMakeLists.txt` 中添加新定义的 action
|
||||
因为在指令集中新建了指令,因此调试时需要编译,并在终端环境中加载临时路径:
|
||||
```bash
|
||||
cd unilabos_msgs
|
||||
colcon build
|
||||
source ./install/local_setup.sh
|
||||
cd ..
|
||||
```
|
||||
|
||||
调试成功后,发起 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
|
||||
```
|
||||
|
||||
3. 在 `unilabos/messages/__init__.py` 中添加 Pydantic 定义的实验操作名和参数列表
|
||||
```{literalinclude} ../../unilabos/messages/__init__.py
|
||||
:start-after: Start Protocols
|
||||
:end-before: End Protocols
|
||||
```
|
||||
|
||||
4. 在 `unilabos/compile` 中新建编译为机器指令的函数,函数入参为设备连接图 `G` 和实验操作参数。
|
||||
```{literalinclude} ../../unilabos/compile/pump_protocol.py
|
||||
:start-after: Pump protocol compilation
|
||||
:end-before: End Protocols
|
||||
```
|
||||
|
||||
5. 将该函数加入 `unilabos/compile/__init__.py` 的 `action_protocol_generators` 中:
|
||||
```{literalinclude} ../../unilabos/compile/__init__.py
|
||||
:start-after: Define
|
||||
:end-before: End Protocols
|
||||
```
|
||||
|
||||
6. 记得将新开发的 `Protocol` 添加至启动时的 `devices.json` 中。
|
||||
```{literalinclude} ../../devices.json
|
||||
:lines: 2-4
|
||||
```
|
||||
95
docs/developer_guide/add_yaml.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# yaml注册表编写指南
|
||||
|
||||
`注册表的结构`
|
||||
|
||||
1. 顶层名称:每个设备的注册表以设备名称开头,例如 new_device。
|
||||
2. class 字段:定义设备的模块路径和类型。
|
||||
3. schema 字段:定义设备的属性模式,包括属性类型、描述和必需字段。
|
||||
4. action_value_mappings 字段:定义设备支持的动作及其目标、反馈和结果。
|
||||
|
||||
`创建新的注册表教程`
|
||||
1. 创建文件
|
||||
在 devices 文件夹中创建一个新的 YAML 文件,例如 new_device.yaml。
|
||||
|
||||
2. 定义设备名称
|
||||
在文件中定义设备的顶层名称,例如:new_device
|
||||
|
||||
3. 定义设备的类信息
|
||||
添加设备的模块路径和类型:
|
||||
|
||||
```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:导航设备通过多个点。
|
||||
|
||||
type: <ActionType> # 动作的类型,表示动作的功能
|
||||
# 根据动作的功能选择合适的类型:
|
||||
# SendCmd:发送简单命令。
|
||||
# NavigateThroughPoses:导航动作。
|
||||
# SingleJointPosition:设置单一关节的位置。
|
||||
# Stir:搅拌动作。
|
||||
# HeatChill:加热或冷却动作。
|
||||
|
||||
goal: # 定义动作的目标值映射,表示需要传递给设备的参数。
|
||||
<goal_key>: <mapped_value> #确定设备需要的输入参数,并将其映射到设备的字段。
|
||||
|
||||
feedback: # 定义动作的反馈值映射,表示设备执行动作时返回的实时状态。
|
||||
<feedback_key>: <mapped_value>
|
||||
result: # 定义动作的结果值映射,表示动作完成后返回的最终结果。
|
||||
<result_key>: <mapped_value>
|
||||
```
|
||||
|
||||
6. 定义设备的属性模式
|
||||
添加设备的属性模式,包括属性类型和描述:
|
||||
```python
|
||||
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
|
||||
```
|
||||
# 写完yaml注册表后需要添加到哪些其他文件?
|
||||
330
docs/developer_guide/device_driver.md
Normal file
@@ -0,0 +1,330 @@
|
||||
# 设备 Driver 开发
|
||||
|
||||
我们对设备 Driver 的定义,是一个 Python/C++/C# 类,类的方法可以用于获取传感器数据、执行设备动作、更新物料信息。它们经过 Uni-Lab 的通信中间件包装,就能成为高效分布式通信的设备节点。
|
||||
|
||||
因此,若已有设备的 SDK (Driver),可以直接 [添加进 Uni-Lab](add_device.md)。仅当没有 SDK (Driver) 时,请参考本章作开发。
|
||||
|
||||
## 有串口字符串指令集文档的设备:Python 串口通信(常见 RS485, RS232, USB)
|
||||
|
||||
开发方式:对照厂家给出的指令集文档,实现相应发送指令字符串的 python 函数。可参考 [注射泵串口驱动样例](https://github.com/TablewareBox/runze-syringe-pump)
|
||||
|
||||
## 常见工业通信协议:Modbus(RTU, TCP)
|
||||
|
||||
Modbus 与 RS485、RS232 不一样的地方在于,会有更多直接寄存器的读写,以及涉及字节序转换(Big Endian, Little Endian)。
|
||||
|
||||
Uni-Lab 开发团队在仓库中提供了3个样例:
|
||||
|
||||
* 单一机械设备**电夹爪**,通讯协议可见 [增广夹爪通讯协议](https://doc.rmaxis.com/docs/communication/fieldbus/),驱动代码位于 `unilabos/devices/gripper/rmaxis_v4.py`
|
||||
* 单一通信设备**IO板卡**,驱动代码位于 `unilabos/device_comms/gripper/SRND_16_IO.py`
|
||||
* 执行多设备复杂任务逻辑的**PLC**,Uni-Lab 提供了基于地址表的接入方式和点动工作流编写,测试代码位于 `unilabos/device_comms/modbus_plc/test/test_workflow.py`
|
||||
|
||||
****
|
||||
|
||||
## 其他工业通信协议:CANopen, Ethernet, OPCUA...
|
||||
|
||||
【敬请期待】
|
||||
|
||||
## 没有接口的老设备老软件:使用 PyWinAuto
|
||||
|
||||
**pywinauto**是一个 Python 库,用于自动化Windows GUI操作。它可以模拟用户的鼠标点击、键盘输入、窗口操作等,广泛应用于自动化测试、GUI自动化等场景。它支持通过两个后端进行操作:
|
||||
|
||||
* **win32**后端:适用于大多数Windows应用程序,使用native Win32 API。(pywinauto_recorder默认使用win32后端)
|
||||
* **uia**后端:基于Microsoft UI Automation,适用于较新的应用程序,特别是基于WPF或UWP的应用程序。(在win10上,会有更全的目录,有的窗口win32会识别不到)
|
||||
|
||||
### windows平台安装pywinauto和pywinauto_recorder
|
||||
|
||||
直接安装会造成环境崩溃,需要下载并解压已经修改好的文件。
|
||||
|
||||
cd到对应目录,执行安装
|
||||
|
||||
`pip install . -i ``https://pypi.tuna.tsinghua.edu.cn/simple`
|
||||
|
||||

|
||||
|
||||
windows平台测试 python pywinauto_recorder.py,退出使用两次ctrl+alt+r取消选中,关闭命令提示符。
|
||||
|
||||
### 计算器例子
|
||||
|
||||
你可以先打开windows的计算器,然后在ilab的环境中运行下面的代码片段,可观察到得到结果,通过这一案例,你需要掌握的pywinauto用法:
|
||||
|
||||
* 连接到指定进程
|
||||
* 利用dump_tree查找需要的窗口
|
||||
* 获取某个位置的信息
|
||||
* 模拟点击
|
||||
* 模拟输入
|
||||
|
||||
#### 代码学习
|
||||
|
||||
```Python
|
||||
from pywinauto import Application
|
||||
import time
|
||||
|
||||
from pywinauto.findwindows import ElementAmbiguousError
|
||||
|
||||
# 启动计算器应用
|
||||
app = Application(backend='uia').connect(title="计算器")
|
||||
|
||||
# 连接到计算器窗口
|
||||
window = app["计算器Dialog0"]
|
||||
|
||||
# 打印窗口控件树结构,帮助定位控件
|
||||
window.dump_tree(depth=3)
|
||||
# 详细输出
|
||||
"""
|
||||
Dialog - '计算器' (L-419, T773, R-73, B1287)
|
||||
['计算器Dialog', 'Dialog', '计算器', '计算器Dialog0', '计算器Dialog1', 'Dialog0', 'Dialog1', '计算器0', '计算器1']
|
||||
child_window(title="计算器", control_type="Window")
|
||||
|
|
||||
| Dialog - '计算器' (L-269, T774, R-81, B806)
|
||||
| ['计算器Dialog2', 'Dialog2', '计算器2']
|
||||
| child_window(title="计算器", auto_id="TitleBar", control_type="Window")
|
||||
| |
|
||||
| | Menu - '系统' (L0, T0, R0, B0)
|
||||
| | ['Menu', '系统', '系统Menu', '系统0', '系统1']
|
||||
| | child_window(title="系统", auto_id="SystemMenuBar", control_type="MenuBar")
|
||||
| |
|
||||
| | Button - '最小化 计算器' (L-219, T774, R-173, B806)
|
||||
| | ['Button', '最小化 计算器Button', '最小化 计算器', 'Button0', 'Button1']
|
||||
| | child_window(title="最小化 计算器", auto_id="Minimize", control_type="Button")
|
||||
| |
|
||||
| | Button - '使 计算器 最大化' (L-173, T774, R-127, B806)
|
||||
| | ['Button2', '使 计算器 最大化', '使 计算器 最大化Button']
|
||||
| | child_window(title="使 计算器 最大化", auto_id="Maximize", control_type="Button")
|
||||
| |
|
||||
| | Button - '关闭 计算器' (L-127, T774, R-81, B806)
|
||||
| | ['Button3', '关闭 计算器Button', '关闭 计算器']
|
||||
| | child_window(title="关闭 计算器", auto_id="Close", control_type="Button")
|
||||
|
|
||||
| Dialog - '计算器' (L-411, T774, R-81, B1279)
|
||||
| ['计算器Dialog3', 'Dialog3', '计算器3']
|
||||
| child_window(title="计算器", control_type="Window")
|
||||
| |
|
||||
| | Static - '计算器' (L-363, T782, R-327, B798)
|
||||
| | ['计算器Static', 'Static', '计算器4', 'Static0', 'Static1']
|
||||
| | child_window(title="计算器", auto_id="AppName", control_type="Text")
|
||||
| |
|
||||
| | Custom - '' (L-411, T806, R-81, B1279)
|
||||
| | ['Custom', '计算器Custom']
|
||||
| | child_window(auto_id="NavView", control_type="Custom")
|
||||
|
|
||||
| Pane - '' (L-411, T806, R-81, B1279)
|
||||
| ['Pane', '计算器Pane']
|
||||
"""
|
||||
|
||||
# 通过控件路径(可参考下一小节路径)可以发现,本文档第48-50行是我们需要定位的控件
|
||||
# 本文档第16-18行为其自身,即depth=1,我们要定位的第48-50行是depth=3的控件,从树来一级一级定位即可
|
||||
# PyWinAuto为我们提供了非常便捷的取窗口方式,在每3行dump的内容中,第三行就是从上一级取出当前窗口的方式,直接复制即可
|
||||
# 这里注意到,使用title="计算器", control_type="Window"进行匹配,会出现两个,因此指定found_index=1
|
||||
sub_window = window.child_window(title="计算器", control_type="Window", found_index=1)
|
||||
target_window = sub_window.child_window(auto_id="NavView", control_type="Custom")
|
||||
target_window.dump_tree(depth=3)
|
||||
"""
|
||||
Custom - '' (L-411, T806, R-81, B1279)
|
||||
['标准Custom', 'Custom']
|
||||
child_window(auto_id="NavView", control_type="Custom")
|
||||
|
|
||||
| Button - '打开导航' (L-407, T812, R-367, B848)
|
||||
| ['打开导航Button', '打开导航', 'Button', 'Button0', 'Button1']
|
||||
| child_window(title="打开导航", auto_id="TogglePaneButton", control_type="Button")
|
||||
| |
|
||||
| | Static - '' (L0, T0, R0, B0)
|
||||
| | ['Static', 'Static0', 'Static1']
|
||||
| | child_window(auto_id="PaneTitleTextBlock", control_type="Text")
|
||||
|
|
||||
| GroupBox - '' (L-411, T814, R-81, B1275)
|
||||
| ['标准GroupBox', 'GroupBox', 'GroupBox0', 'GroupBox1']
|
||||
| |
|
||||
| | Static - '表达式为 ' (L0, T0, R0, B0)
|
||||
| | ['表达式为 ', 'Static2', '表达式为 Static']
|
||||
| | child_window(title="表达式为 ", auto_id="CalculatorExpression", control_type="Text")
|
||||
| |
|
||||
| | Static - '显示为 0' (L-411, T875, R-81, B947)
|
||||
| | ['显示为 0Static', '显示为 0', 'Static3']
|
||||
| | child_window(title="显示为 0", auto_id="CalculatorResults", control_type="Text")
|
||||
| |
|
||||
| | Button - '打开历史记录浮出控件' (L-121, T814, R-89, B846)
|
||||
| | ['打开历史记录浮出控件', '打开历史记录浮出控件Button', 'Button2']
|
||||
| | child_window(title="打开历史记录浮出控件", auto_id="HistoryButton", control_type="Button")
|
||||
| |
|
||||
| | GroupBox - '记忆控件' (L-407, T948, R-85, B976)
|
||||
| | ['记忆控件', '记忆控件GroupBox', 'GroupBox2']
|
||||
| | child_window(title="记忆控件", auto_id="MemoryPanel", control_type="Group")
|
||||
| |
|
||||
| | GroupBox - '显示控件' (L-407, T978, R-85, B1026)
|
||||
| | ['显示控件', 'GroupBox3', '显示控件GroupBox']
|
||||
| | child_window(title="显示控件", auto_id="DisplayControls", control_type="Group")
|
||||
| |
|
||||
| | GroupBox - '标准函数' (L-407, T1028, R-166, B1076)
|
||||
| | ['标准函数', '标准函数GroupBox', 'GroupBox4']
|
||||
| | child_window(title="标准函数", auto_id="StandardFunctions", control_type="Group")
|
||||
| |
|
||||
| | GroupBox - '标准运算符' (L-164, T1028, R-85, B1275)
|
||||
| | ['标准运算符', '标准运算符GroupBox', 'GroupBox5']
|
||||
| | child_window(title="标准运算符", auto_id="StandardOperators", control_type="Group")
|
||||
| |
|
||||
| | GroupBox - '数字键盘' (L-407, T1078, R-166, B1275)
|
||||
| | ['GroupBox6', '数字键盘', '数字键盘GroupBox']
|
||||
| | child_window(title="数字键盘", auto_id="NumberPad", control_type="Group")
|
||||
| |
|
||||
| | Button - '正负' (L-407, T1228, R-328, B1275)
|
||||
| | ['Button32', '正负Button', '正负']
|
||||
| | child_window(title="正负", auto_id="negateButton", control_type="Button")
|
||||
|
|
||||
| Static - '标准' (L-363, T815, R-322, B842)
|
||||
| ['标准', '标准Static', 'Static4']
|
||||
| child_window(title="标准", auto_id="Header", control_type="Text")
|
||||
|
|
||||
| Button - '始终置顶' (L-312, T814, R-280, B846)
|
||||
| ['始终置顶Button', '始终置顶', 'Button33']
|
||||
| child_window(title="始终置顶", auto_id="NormalAlwaysOnTopButton", control_type="Button")
|
||||
"""
|
||||
# 观察到GroupBox控件并没有提供默认的child_window,而list中的identifier均可作为best_match来索引
|
||||
# ['标准GroupBox', 'GroupBox', 'GroupBox0', 'GroupBox1'] 这里选用第0项
|
||||
group_box = target_window.child_window(best_match="标准GroupBox")
|
||||
numpad = group_box.child_window(title="数字键盘", auto_id="NumberPad", control_type="Group")
|
||||
numpad.dump_tree(depth=2)
|
||||
"""
|
||||
GroupBox - '数字键盘' (L-334, T1350, R-93, B1547)
|
||||
['GroupBox', '数字键盘', '数字键盘GroupBox']
|
||||
child_window(title="数字键盘", auto_id="NumberPad", control_type="Group")
|
||||
|
|
||||
| Button - '零' (L-253, T1500, R-174, B1547)
|
||||
| ['零Button', 'Button', '零', 'Button0', 'Button1']
|
||||
| child_window(title="零", auto_id="num0Button", control_type="Button")
|
||||
|
|
||||
| Button - '一' (L-334, T1450, R-255, B1498)
|
||||
| ['一Button', 'Button2', '一']
|
||||
| child_window(title="一", auto_id="num1Button", control_type="Button")
|
||||
|
|
||||
| Button - '二' (L-253, T1450, R-174, B1498)
|
||||
| ['Button3', '二', '二Button']
|
||||
| child_window(title="二", auto_id="num2Button", control_type="Button")
|
||||
|
|
||||
| Button - '三' (L-172, T1450, R-93, B1498)
|
||||
| ['Button4', '三', '三Button']
|
||||
| child_window(title="三", auto_id="num3Button", control_type="Button")
|
||||
|
|
||||
| Button - '四' (L-334, T1400, R-255, B1448)
|
||||
| ['四', 'Button5', '四Button']
|
||||
| child_window(title="四", auto_id="num4Button", control_type="Button")
|
||||
|
|
||||
| Button - '五' (L-253, T1400, R-174, B1448)
|
||||
| ['Button6', '五Button', '五']
|
||||
| child_window(title="五", auto_id="num5Button", control_type="Button")
|
||||
|
|
||||
| Button - '六' (L-172, T1400, R-93, B1448)
|
||||
| ['六Button', 'Button7', '六']
|
||||
| child_window(title="六", auto_id="num6Button", control_type="Button")
|
||||
|
|
||||
| Button - '七' (L-334, T1350, R-255, B1398)
|
||||
| ['Button8', '七Button', '七']
|
||||
| child_window(title="七", auto_id="num7Button", control_type="Button")
|
||||
|
|
||||
| Button - '八' (L-253, T1350, R-174, B1398)
|
||||
| ['八', 'Button9', '八Button']
|
||||
| child_window(title="八", auto_id="num8Button", control_type="Button")
|
||||
|
|
||||
| Button - '九' (L-172, T1350, R-93, B1398)
|
||||
| ['Button10', '九', '九Button']
|
||||
| child_window(title="九", auto_id="num9Button", control_type="Button")
|
||||
|
|
||||
| Button - '十进制分隔符' (L-172, T1500, R-93, B1547)
|
||||
| ['十进制分隔符Button', 'Button11', '十进制分隔符']
|
||||
| child_window(title="十进制分隔符", auto_id="decimalSeparatorButton", control_type="Button")
|
||||
"""
|
||||
# 获取按钮 '9'
|
||||
button_9 = numpad.child_window(title="九", auto_id="num9Button", control_type="Button")
|
||||
# 利用相同的办法,我们也可以找到增加和等于号的控件
|
||||
std_calc_panel = group_box.child_window(title="标准运算符", auto_id="StandardOperators", control_type="Group")
|
||||
equal_operation = std_calc_panel.child_window(title="等于", auto_id="equalButton", control_type="Button")
|
||||
|
||||
# 模拟点击按钮 '9'
|
||||
button_9.click_input()
|
||||
# 键入:https://github.com/pywinauto/pywinauto/blob/atspi/pywinauto/windows/keyboard.py
|
||||
# 模拟输入 '加号' 和 数字9
|
||||
window.type_keys("{VK_ADD}9")
|
||||
# 等于
|
||||
equal_operation.click_input()
|
||||
# 获取计算结果文本(显示在计算器窗口的文本框中)
|
||||
result = group_box.child_window(auto_id="CalculatorResults", control_type="Text").window_text()
|
||||
print(f"计算结果:{result[4:]}") # 应当得到结果18
|
||||
```
|
||||
|
||||
#### 依据像素判定状态
|
||||
|
||||
```Python
|
||||
# 有时,你需要根据窗口的颜色判断是否可用,是否正在运行,可以使用pyautogui来实现这一功能
|
||||
# pyautogui需要对应在环境中进行安装
|
||||
point_x = int(control_view.rectangle().left + control_view.rectangle().width() * 0.15)
|
||||
point_y = 15 + control_view.rectangle().top
|
||||
r, g, b = pyautogui.pixel(point_x, point_y)
|
||||
```
|
||||
|
||||
### pywinauto_recorder
|
||||
|
||||
pywinauto_recorder是一个配合 pywinauto 使用的工具,用于录制用户的操作,并生成相应的 pywinauto 脚本。这对于一些暂时无法直接调用DLL的函数并且需要模拟用户操作的场景非常有用。同时,可以省去仅用pywinauto的一些查找UI步骤。
|
||||
|
||||
#### 运行尝试
|
||||
|
||||
请参照 上手尝试-环境创建-3 开启pywinauto_recorder
|
||||
|
||||
例如我们这里先启动一个windows自带的计算器软件
|
||||
|
||||

|
||||
|
||||
在录制状态下点击数字键盘的“9”,随后退出录制,得到下图运行的文件。
|
||||
|
||||

|
||||
|
||||
```Python
|
||||
# encoding: utf-8
|
||||
|
||||
from pywinauto_recorder.player import *
|
||||
|
||||
with UIPath(u"计算器||Window"):
|
||||
with UIPath(u"计算器||Window->||Custom->||Group->数字键盘||Group"):
|
||||
click(u"九||Button")
|
||||
```
|
||||
|
||||
执行该python脚本,可以观察到新开启的计算器被点击了数字9
|
||||
|
||||

|
||||
|
||||
### `dump_tree`详解
|
||||
|
||||
`dump_tree`方法用于打印控件树结构,帮助我们快速了解应用程序窗口中的控件层级,尤其是在自动化测试或脚本开发中,识别控件非常重要。
|
||||
|
||||
```Python
|
||||
window.dump_tree(depth=[int类型数字], filename=None)
|
||||
# 打印当前窗口及其子控件的树结构
|
||||
# 在debug的过程中,如果需要查找某一控件,可以通过depth指定为4~5,利用搜索查看是哪个独立控件
|
||||
# 指定filename后将保存到对应目录文件中
|
||||
```
|
||||
|
||||
输出会列出窗口的各个控件及其子控件,显示每个控件的属性(如标题、类型等)。
|
||||
|
||||
```Python
|
||||
"""
|
||||
GroupBox - '数字键盘' (L-334, T1350, R-93, B1547)
|
||||
['GroupBox', '数字键盘', '数字键盘GroupBox']
|
||||
child_window(title="数字键盘", auto_id="NumberPad", control_type="Group")
|
||||
|
|
||||
| Button - '零' (L-253, T1500, R-174, B1547)
|
||||
| ['零Button', 'Button', '零', 'Button0', 'Button1']
|
||||
| child_window(title="零", auto_id="num0Button", control_type="Button")
|
||||
"""
|
||||
```
|
||||
|
||||
这里以上面计算器的例子对dump_tree进行解读
|
||||
|
||||
2~4行为当前对象的窗口
|
||||
|
||||
* 第2行分别是窗体的类型 `GroupBox`,窗体的题目 `数字键盘`,窗体的矩形区域坐标,对应的是屏幕上的位置(左、上、右、下)
|
||||
* 第3行是 `['GroupBox', '数字键盘', '数字键盘GroupBox']`,为控件的标识符列表,可以选择任意一个,使用 `child_window(best_match="标识符")`来获取该窗口
|
||||
* 第4行是获取该控件的方法,请注意该方法不能保证获取唯一,`title`如果是变化的,也需要删除 `title`参数
|
||||
|
||||
6~8行为当前对象窗口所包含的子窗口信息,信息类型对应2~4行
|
||||
|
||||
### 窗口获取注意事项
|
||||
|
||||
1. 在 `child_window`的时候,并不会立刻报错,只有在执行窗口的信息获取时才会调用,查询窗口是否存在,因此要想确定 `child_window`是否正确,可以调用子窗口对象的属性 `element_info`,来保证窗口存在
|
||||
BIN
docs/developer_guide/image/device_driver/calculator_01.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
docs/developer_guide/image/device_driver/calculator_02.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
docs/developer_guide/image/device_driver/calculator_03.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
docs/developer_guide/image/device_driver/pywinauto_install.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
9
docs/index.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Uni-Lab 项目文档
|
||||
|
||||
欢迎来到项目文档的首页!
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 3
|
||||
|
||||
intro.md
|
||||
```
|
||||
44
docs/intro.md
Normal file
@@ -0,0 +1,44 @@
|
||||
欢迎来到项目文档的首页!
|
||||
|
||||
## 核心概念
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
concepts/overview.md
|
||||
concepts/01-communication-instruction.md
|
||||
concepts/02-topology-and-chemputer-compile.md
|
||||
```
|
||||
|
||||
## **用户指南**
|
||||
|
||||
本指南将带你了解如何使用项目的功能。
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
user_guide/installation.md
|
||||
user_guide/configuration.md
|
||||
user_guide/launch.md
|
||||
boot_examples/index.md
|
||||
```
|
||||
|
||||
## 开发者指南
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
developer_guide/device_driver
|
||||
developer_guide/add_device
|
||||
developer_guide/add_action
|
||||
developer_guide/actions
|
||||
developer_guide/add_protocol
|
||||
```
|
||||
|
||||
## 接口文档
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
apidocs/unilabos
|
||||
```
|
||||
35
docs/make.bat
Normal file
@@ -0,0 +1,35 @@
|
||||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=.
|
||||
set BUILDDIR=_build
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.https://www.sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||
|
||||
:end
|
||||
popd
|
||||
120
docs/user_guide/configuration.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# Uni-Lab 配置指南
|
||||
|
||||
Uni-Lab支持通过Python配置文件进行灵活的系统配置。本指南将帮助您理解配置选项并设置您的Uni-Lab环境。
|
||||
|
||||
## 配置文件格式
|
||||
|
||||
Uni-Lab支持Python格式的配置文件,它比YAML或JSON提供更多的灵活性,包括支持注释、条件逻辑和复杂数据结构。
|
||||
|
||||
### 基本配置示例
|
||||
|
||||
一个典型的配置文件包含以下部分:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python
|
||||
# coding=utf-8
|
||||
"""Uni-Lab 配置文件"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
# 配置类定义
|
||||
@dataclass
|
||||
class MQConfig:
|
||||
"""MQTT 配置类"""
|
||||
lab_id: str = "YOUR_LAB_ID"
|
||||
# 更多配置...
|
||||
|
||||
# 其他配置类...
|
||||
```
|
||||
|
||||
## 配置选项说明
|
||||
|
||||
### MQTT配置 (MQConfig)
|
||||
|
||||
MQTT配置用于连接消息队列服务,是Uni-Lab与云端通信的主要方式。
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
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"
|
||||
cert_file: str = "/path/to/cert.pem"
|
||||
key_file: str = "/path/to/key.pem"
|
||||
|
||||
# 或者直接提供证书内容
|
||||
ca_content: str = ""
|
||||
cert_content: str = ""
|
||||
key_content: str = ""
|
||||
```
|
||||
|
||||
#### 证书配置
|
||||
|
||||
MQTT连接支持两种方式配置证书:
|
||||
|
||||
1. **文件路径方式**(推荐):指定证书文件的路径,系统会自动读取文件内容
|
||||
2. **直接内容方式**:直接在配置中提供证书内容
|
||||
|
||||
推荐使用文件路径方式,便于证书的更新和管理。
|
||||
|
||||
### HTTP客户端配置 (HTTPConfig)
|
||||
|
||||
即将开放 Uni-Lab 云端实验室。
|
||||
|
||||
### ROS模块配置 (ROSConfig)
|
||||
|
||||
配置ROS消息转换器需要加载的模块:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class ROSConfig:
|
||||
"""ROS模块配置"""
|
||||
modules: list = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.modules is None:
|
||||
self.modules = [
|
||||
"std_msgs.msg",
|
||||
"geometry_msgs.msg",
|
||||
"control_msgs.msg",
|
||||
"control_msgs.action",
|
||||
"nav2_msgs.action",
|
||||
"unilabos_msgs.msg",
|
||||
"unilabos_msgs.action",
|
||||
]
|
||||
```
|
||||
|
||||
您可以根据需要添加其他ROS模块。
|
||||
|
||||
### 其他配置选项
|
||||
|
||||
- **OSSUploadConfig**: 对象存储上传配置
|
||||
|
||||
## 如何使用配置文件
|
||||
|
||||
启动Uni-Lab时通过`--config`参数指定配置文件路径:
|
||||
|
||||
```bash
|
||||
unilab --config path/to/your/config.py
|
||||
```
|
||||
|
||||
## 环境变量覆盖
|
||||
|
||||
某些配置项可以通过环境变量进行覆盖,这在不同环境部署时特别有用:
|
||||
|
||||
```bash
|
||||
# 设置环境变量覆盖配置
|
||||
export UNILAB_LAB_ID="YOUR_LAB_ID"
|
||||
export UNILAB_MQTT_BROKER="mqtt-broker-address"
|
||||
|
||||
# 启动Uni-Lab
|
||||
python -m unilabos.app.main --config path/to/your/config.py
|
||||
```
|
||||
24
docs/user_guide/installation.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# **Uni-Lab 安装**
|
||||
|
||||
请先 `git clone` 本仓库,随后按照以下步骤安装项目:
|
||||
|
||||
`Uni-Lab` 建议您采用 `mamba` 管理环境。若需从头建立 `Uni-Lab` 的运行依赖环境,请执行
|
||||
|
||||
```shell
|
||||
mamba env create -f unilabos-<YOUR_OS>.yaml
|
||||
mamba activate ilab
|
||||
```
|
||||
|
||||
其中 `YOUR_OS` 是您的操作系统,可选值 `win64`, `linux-64`, `osx-64`, `osx-arm64`
|
||||
|
||||
若需将依赖安装进当前环境,请执行
|
||||
|
||||
```shell
|
||||
conda env update --file unilabos-<YOUR_OS>.yml
|
||||
```
|
||||
|
||||
随后,可在本仓库安装 `unilabos` 的开发版:
|
||||
|
||||
```shell
|
||||
pip install .
|
||||
```
|
||||
77
docs/user_guide/launch.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Uni-Lab 启动
|
||||
|
||||
安装完毕后,可以通过 `unilab` 命令行启动:
|
||||
|
||||
```bash
|
||||
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.
|
||||
-c CONTROLLERS, --controllers CONTROLLERS
|
||||
Controllers config file.
|
||||
--registry_path REGISTRY_PATH
|
||||
Path to the registry
|
||||
--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
|
||||
```
|
||||
|
||||
## 使用配置文件
|
||||
|
||||
Uni-Lab支持使用Python格式的配置文件进行系统设置。通过 `--config` 参数指定配置文件路径:
|
||||
|
||||
```bash
|
||||
# 使用配置文件启动
|
||||
unilab --config path/to/your/config.py
|
||||
```
|
||||
|
||||
配置文件包含MQTT、HTTP、ROS等系统设置。有关配置文件的详细信息,请参阅[配置指南](configuration.md)。
|
||||
|
||||
## 初始化信息来源
|
||||
|
||||
启动 Uni-Lab 时,可以选用两种方式之一配置实验室设备、耗材、通信、控制逻辑:
|
||||
|
||||
### 1. 组态&拓扑图
|
||||
|
||||
使用 `-g` 时,组态&拓扑图应包含实验室所有信息,详见{ref}`graph`。目前支持 graphml 和 node-link json 两种格式。格式可参照 `tests/experiments` 下的启动文件。
|
||||
|
||||
### 2. 分别指定设备、耗材、控制逻辑
|
||||
|
||||
分别使用 `-d, -r, -c` 依次传入设备组态配置、耗材列表、控制逻辑。
|
||||
|
||||
可参照 `devices.json` 和 `resources.json`。
|
||||
|
||||
不管使用哪一种初始化方式,设备/物料字典均需包含 `class` 属性,用于查找注册表信息。默认查找范围都是 Uni-Lab 内部注册表 `unilabos/registry/{devices,device_comms,resources}`。要添加额外的注册表路径,可以使用 `--registry` 加入 `<your-registry-path>/{devices,device_comms,resources}`。
|
||||
|
||||
## 通信中间件 `--backend`
|
||||
|
||||
目前 Uni-Lab 仅支持 ros2 作为通信中间件。
|
||||
|
||||
## 端云桥接 `--app_bridges`
|
||||
|
||||
目前 Uni-Lab 提供 FastAPI (http), MQTT 两种端云通信方式。其中默认 MQTT 负责端对云状态同步和云对端任务下发,FastAPI 负责端对云物料更新。
|
||||
|
||||
## 分布式组网
|
||||
|
||||
启动 Uni-Lab 时,加入 `--without_host` 将作为从站,不加将作为主站,主站 (host) 持有物料修改权以及对云端的通信。局域网内分别启动的 Uni-Lab 主站/从站将自动组网,互相能访问所有设备状态、传感器信息并发送指令。
|
||||
|
||||
## 完整启动示例
|
||||
|
||||
以下是一些常用的启动命令示例:
|
||||
|
||||
```bash
|
||||
# 使用配置文件和组态图启动
|
||||
unilab -g path/to/graph.json
|
||||
|
||||
# 使用配置文件和分离的设备/资源文件启动
|
||||
unilab -d devices.json -r resources.json
|
||||
```
|
||||