Initial commit

This commit is contained in:
Junhan Chang
2025-04-17 15:19:47 +08:00
parent a47a3f5c3a
commit c78ac482d8
262 changed files with 39871 additions and 0 deletions

20
docs/Makefile Normal file
View 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)

View File

@@ -0,0 +1,10 @@
# 启动样例
本章节提供了几个典型的启动样例,帮助您快速了解和使用系统。每个样例都包含了详细的配置说明、文件解读以及操作步骤,便于您参考和实践。
```{toctree}
:maxdepth: 1
liquid_handler.md
organic_synthesis.md
```

View 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接口进行调用以实现复杂的移液流程。

View 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 在`主机节点信息`-`设备状态`查看该操作对设备开关的实时效果。

View 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/)**
典型的话题通信有:
* 传感器连续发送设备状态和数据;
* 连续时间控制器发送控制指令,如控温、连续称量、机械臂轨迹跟随、视觉识别操作等
![](image/01-communication-instruction/topic.gif)
### **[服务(短时请求与响应)](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/)**
典型的服务通信有:
* 查/改全局参数如物料、设备
* 使用其他通信接口发送/接收数据
![](image/01-communication-instruction/service.gif)
### **[动作(长时任务启动,随后连续收到反馈值,直到达到目标)](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/)**
动作机制主要用于处理运行时长较长的单点任务或任务组合,如:
* 执行工作流
* 执行工作流的子动作
![](image/01-communication-instruction/action.gif)
## 通信指令集
Uni-Lab 目前使用 ROS2 作为通信中间件,因此大量使用其标准消息作为话题、服务、动作。新增指令位于仓库中的 `unilabos_msgs` ,各类实验动作指令集分类整理于 {ref}`actions`
## 通信中间件层
通信中间件层的一个重要设计思想是:将业务逻辑开发,与实际部署中的通信和运行解耦。开发者在实现具体业务逻辑时,可以不用关心最终运行时的 **部署方式****通信方式** 。当用户开发完成后,再根据实际情况决定部署、通信方案。
* 对于 **“流动化学实验室”和“桌面机器人”** 来说,一台电脑通过串口控制所有设备足够。**此时在这台电脑启动 Uni-Lab 作为 Server 即可。**
* 对于 **“移动机器人”大型实验室** ,典型场景是,一个实验室由多台不同位置的工作站组成,每台大型设备有一台工控机,通过串口再控制子设备。同时有 AGV/机械臂 负责转运。**此时,在每台工控机启动 Uni-Lab完成通信中间件层的包装之后只要处于同一局域网下他们将能自动互相发现并组成分布式的“Uni-Lab-Edge Server”。**
* 通信中间件层的分布式机制,使得 Node 之间做好了隔离,一台设备故障时只需重启单个 Node。很像微服务、微内核的设计理念。

View File

@@ -0,0 +1,79 @@
(graph)=
# 实验室组态图
组态(configuration)图是指在自动化领域中,用来描述和展示控制系统中各个组件之间关系的图形化表示方法。
它是一个系统的框架图,通过图形符号和连接线,将各个组件(如传感器、执行器、控制器等)以及它们之间的关系进行可视化展示。
Uni-Lab 的组态图当前支持 node-link json 和 graphml 格式其中包含4类重要信息
* 单个设备/物料配置,即图中节点的参数;
* 父子关系,如一台工作站包含它的多个子设备、放置着多个物料耗材;
* 物理连接关系如流体管路连接、AGV/机械臂/直线模组转运连接。
* 通信转接关系,如多个 IO 设备通过 IO 板卡或 PLC 转为 Modbus串口转网口等
* 控制逻辑关系,如某个输出量被某个输入量 PID 控制
## 父子关系、物质流与"编译"操作
在计算机操作系统下,软件操作数据和文件。在实验操作系统下,实验“软件”利用仪器“硬件”操作物质。实验人员能理解的操作,最终都是对物质的处理。将实验步骤,转化为硬件指令,这个操作我们可以类比为“编译”。
对用户来说,“直接操作设备执行单个指令”不是个真实需求,真正的需求是**“执行对实验有意义的单个完整动作”——加入某种液体多少量;萃取分液;洗涤仪器等等。就像实验步骤文字书写的那样。**
而这些对实验有意义的单个完整动作,**一般需要多个设备的协同**,还依赖于他们的**物理连接关系(管道相连;机械臂可转运)**。
于是 Uni-Lab 实现了抽象的“工作站”,即注册表中的 `workstation` 设备(`ProtocolNode`类)来处理编译、规划操作。以泵骨架组成的自动有机实验室为例,设备管道连接关系如下:
![topology](image/02-topology-and-chemputer-compile/topology.png)
接收“移液”动作,编译为一系列泵指令和阀指令
```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` 目录。
![configuration](image/01-communication-instruction/configuration.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 KiB

View File

@@ -0,0 +1,3 @@
# Uni-Lab 操作系统总览
![Layers](image/overview/Uni-Lab-layers.png)![Layers](image/overview/Uni-Lab-whiteboard.png)

205
docs/conf.py Normal file
View 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)

View 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
```
----

View File

@@ -0,0 +1,7 @@
(actions)=
# Uni-Lab 动作指令集
Uni-Lab 当前动作指令集设计时,主要考虑兼容领域历史开源工具。目前包括以下场景:
```{include} action_includes.md
```

View 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 requestUni-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
```

View 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
```

View 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 requestUni-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
```

View 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注册表后需要添加到哪些其他文件

View 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)
## 常见工业通信协议ModbusRTU, 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`
![pywinauto_install](image/device_driver/pywinauto_install.png)
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自带的计算器软件
![calculator_01](image/device_driver/calculator_01.png)
在录制状态下点击数字键盘的“9”随后退出录制得到下图运行的文件。
![calculator_02](image/device_driver/calculator_02.png)
```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
![calculator_03](image/device_driver/calculator_03.png)
### `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`,来保证窗口存在

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

9
docs/index.md Normal file
View File

@@ -0,0 +1,9 @@
# Uni-Lab 项目文档
欢迎来到项目文档的首页!
```{toctree}
:maxdepth: 3
intro.md
```

44
docs/intro.md Normal file
View 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
View 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

View 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
```

View 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
View 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
```