Files
Uni-Lab-OS/docs/user_guide/best_practice.md
2025-11-18 18:43:29 +08:00

1838 lines
48 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Uni-Lab-OS 最佳实践指南
## 关于本指南
本指南将引导您从零开始完成 Uni-Lab-OS 实验室系统的完整搭建,从环境安装到高级设备开发。无论您是初次接触 Uni-Lab-OS 的用户,还是希望深入定制开发的开发者,都能在本指南中找到清晰的步骤和实用建议。
### 适用对象
- **实验室管理员**:负责实验室系统部署和维护
- **实验操作人员**:日常使用系统进行实验操作
- **设备开发者**:为实验室添加自定义设备和功能
- **系统集成商**:集成多个实验室系统
### 完整流程概览
```
环境准备 → 创建实验室账号 → 系统启动 → 上传注册表
在线创建设备图 → 测试系统运行 → 运行工作流
多节点部署(可选)→ 自定义设备开发 → 复杂工作站搭建
```
---
## 第一部分:快速上手
### 1. 环境准备(主机 Host
#### 1.1 安装 Uni-Lab-OS
详细的安装步骤请参考 [安装指南](installation.md)。
**关键步骤:**
```bash
# 1. 安装 Mamba如果尚未安装
# 下载 Miniforge: https://github.com/conda-forge/miniforge/releases
# 2. 创建 Conda 环境
mamba create -n unilab python=3.11.11
# 3. 激活环境
mamba activate unilab
# 4. 安装 Uni-Lab-OS
mamba install uni-lab::unilabos -c robostack-staging -c conda-forge
```
#### 1.2 验证安装
```bash
# 检查 unilabos 是否安装成功
python -c "import unilabos; print(unilabos.__version__)"
# 验证 ROS 消息包
python -c "from unilabos_msgs.msg import Resource; print('ROS msgs OK')"
```
如果两条命令都正常输出,说明安装成功。
---
### 2. 创建您的第一个实验室
#### 2.1 注册实验室账号
1. 访问 [https://uni-lab.bohrium.com](https://uni-lab.bohrium.com)
2. 注册账号并登录
3. 创建新实验室
#### 2.2 获取 Access Key 和 Secret Key
1. 在实验室管理界面,找到"密钥管理"
2. 创建新的 AK/SK 密钥对
3. **妥善保管这些密钥**(类似于密码,不要泄露)
![复制 AK/SK](image/copy_aksk.gif)
**重要提示:**
- AK (Access Key):实验室的唯一标识
- SK (Secret Key):实验室的访问密码
- 这两个密钥在后续启动命令中都需要使用
#### 2.3 首次启动配置
首次启动时,系统会自动引导您创建配置文件:
```bash
# 首次启动
conda activate unilab
unilab --ak your_ak --sk your_sk
```
系统会询问是否创建配置文件,输入 `Y` 确认。配置文件会自动生成在 `./unilabos_data/local_config.py`
更多配置选项请参考 [配置指南](../advanced_usage/configuration.md)。
---
### 3. 启动系统
详细的启动参数说明请参考 [启动指南](launch.md)。
#### 3.1 基本启动命令
```bash
conda activate unilab
unilab --ak your_ak --sk your_sk -g path/to/graph.json
```
**关键参数说明:**
- `--ak`:您的 Access Key必需
- `--sk`:您的 Secret Key必需
- `-g`:设备组态图文件路径(必需)
#### 3.2 首次启动注意事项
**第一次启动时,如果没有组态图文件:**
由于首次启动需要 `-g` 参数指定组态图,您可以:
**方案 1使用预先创建好的示例组态图推荐**
Uni-Lab-OS 在安装时已经预置了大量真实的设备图文件示例,无需下载或创建,直接使用 `-g` 参数指定即可:
```bash
# 使用简单的工作台示例(推荐新手)
unilab --ak your_ak --sk your_sk -g test/experiments/workshop.json
# 使用 PRCXI 液体处理工作站示例
unilab --ak your_ak --sk your_sk -g test/experiments/prcxi_9300.json
# 使用格林纳德反应工作站示例(有机合成)
unilab --ak your_ak --sk your_sk -g test/experiments/Grignard_flow_batchreact_single_pumpvalve.json
# 使用 Bioyond 配液站示例
unilab --ak your_ak --sk your_sk -g test/experiments/dispensing_station_bioyond.json
# 使用 HPLC 分析系统示例
unilab --ak your_ak --sk your_sk -g test/experiments/HPLC.json
# 使用空设备配置(最小化配置)
unilab --ak your_ak --sk your_sk -g test/experiments/empty_devices.json
```
**更多可用示例**:系统预置了 40+ 个组态图文件,涵盖液体处理、有机合成、分析检测等多个领域。完整列表请查看 `unilabos/test/experiments/` 目录。
**方案 2创建一个空的组态图**
如果您想从零开始配置,可以创建一个简单的主机节点配置:
```bash
# 创建 example_empty.json
cat > example_empty.json << 'EOF'
{
"nodes": [],
"links": []
}
EOF
# 使用该配置启动
unilab --ak your_ak --sk your_sk -g example_empty.json
```
> **提示**:详细的组态图文件编写指南,请参考 [设备图文件说明](graph_files.md)。
**启动成功标志:**
- 终端显示 `[Host Node] Host node initialized.`
- 自动打开浏览器,显示 Web 管理界面
- 地址:`http://localhost:8002`
#### 3.3 常用启动选项
```bash
# 禁用自动打开浏览器
unilab --ak your_ak --sk your_sk -g graph.json --disable_browser
# 使用不同端口
unilab --ak your_ak --sk your_sk -g graph.json --port 8080
# 测试环境
unilab --addr test --ak your_ak --sk your_sk -g graph.json
# 跳过环境检查(加快启动)
unilab --ak your_ak --sk your_sk -g graph.json --skip_env_check
```
---
### 4. 上传注册表(一次性操作)
#### 4.1 什么是注册表上传?
注册表包含您的设备和物料的完整定义。上传到云端后,在线界面才能识别和使用这些设备。
#### 4.2 何时需要上传?
**必须上传的情况:**
- 首次启动实验室
- 添加了新的设备类型
- 修改了设备的注册表定义
#### 4.3 如何上传注册表
```bash
unilab --ak your_ak --sk your_sk -g graph.json --upload_registry
```
**性能影响说明:**
- 上传注册表会增加启动时间(通常 5-15 秒)
- 上传时间取决于:
- 设备和物料的数量
- 网络速度
- 建议:开发调试时首次上传,后续本地测试可省略
**验证上传成功:**
在 Web 界面的"仪器设备"或"物料耗材"模块中,应该能看到您的设备和物料列表。
---
### 5. 在线创建您的第一个设备图
#### 5.1 使用预设组态图(推荐新手)
Uni-Lab-OS 在安装时已经包含了大量真实的设备图文件,位于 `unilabos/test/experiments/` 目录,涵盖:
- 液体处理PRCXI、Biomek 等自动化移液工作站
- 有机合成:格林纳德反应、流动化学等工作站
- 分析检测HPLC、光谱仪等分析仪器
- 生物实验Bioyond 配液站、反应站等
- 协议测试:各种实验协议的测试工作站
- 虚拟设备:用于开发调试的 mock 设备
**常用示例组态图:**
| 组态图文件 | 说明 | 适合场景 |
| ------------------------------------------------ | -------------------- | -------------------- |
| `workshop.json` | 综合工作台 | 新手入门推荐 |
| `empty_devices.json` | 空设备配置 | 最小化配置,快速启动 |
| `prcxi_9300.json` | PRCXI 液体处理工作站 | 液体处理自动化 |
| `Grignard_flow_batchreact_single_pumpvalve.json` | 格林纳德反应工作站 | 有机合成流动化学 |
| `dispensing_station_bioyond.json` | Bioyond 配液站 | 生物样品配液 |
| `reaction_station_bioyond.json` | Bioyond 反应站 | 生物化学反应 |
| `HPLC.json` | HPLC 分析系统 | 色谱分析 |
| `plr_test.json` | PyLabRobot 测试 | PyLabRobot 集成测试 |
| `mock_devices/mock_all.json` | 完整虚拟设备集 | 开发调试,离线测试 |
**使用方法:**
```bash
# 使用简单工作台(推荐新手第一次启动)
unilab --ak your_ak --sk your_sk -g test/experiments/workshop.json
# 使用 PRCXI 液体处理工作站
unilab --ak your_ak --sk your_sk -g test/experiments/prcxi_9300.json
# 使用格林纳德反应工作站(有机合成)
unilab --ak your_ak --sk your_sk -g test/experiments/Grignard_flow_batchreact_single_pumpvalve.json
# 使用虚拟设备(无需真实硬件,用于学习和测试)
unilab --ak your_ak --sk your_sk -g test/experiments/mock_devices/mock_all.json
```
**注意**:所有组态图文件都是真实项目中使用的配置,可以直接学习和参考。完整文件列表请查看 `unilabos/test/experiments/` 目录。
> 关于设备图文件的结构、字段定义、格式兼容性等完整指南,请参考 [设备图文件说明](graph_files.md)。
#### 5.2 访问 Web 界面
启动系统后,访问[https://uni-lab.bohrium.com](https://uni-lab.bohrium.com)
#### 5.3 添加设备和物料
进入"仪器耗材"模块:
**示例场景:** 创建一个简单的液体转移实验
1. **添加工作站(必需):**
- 在"仪器设备"中找到 `work_station`
- 添加 `workstation` x1
2. **添加虚拟转移泵:**
- 在"仪器设备"中找到 `virtual_device`
- 添加 `virtual_transfer_pump` x1
3. **添加容器:**
- 在"物料耗材"中找到 `container`
- 添加 `container` x2
![物料列表](image/material.png)
#### 5.4 建立设备连接关系
将设备和物料拖拽到画布上,建立父子关系:
1. 将两个 `container` 拖拽到 `workstation`
2.`virtual_transfer_pump` 拖拽到 `workstation`
3. 在画布上连接它们(模拟真实的物理连接)
![设备连接](image/links.png)
**连接规则:**
- 所有设备和物料都必须有父节点(除了顶层工作站)
- 连接关系反映真实的物理布局
- 连接后才能在工作流中使用
#### 5.5 保存组态图
1. 点击"保存"按钮
2. 系统会生成 JSON 格式的组态图
3. 可以下载保存为本地文件,下次使用 `-g` 参数启动
---
### 6. 测试实验室运行状态
在运行实际工作流之前,建议先测试系统是否正常运行。
#### 6.1 使用 host_node 的 test_latency
`test_latency` 是 host_node 提供的测试方法,用于:
- 测试网络延迟
- 校准时间误差
- 验证系统响应
**通过 Web 界面测试:**
1. 在 Web 界面找到"工作流"模块
2. 新建空白工作流
3. 右键画布空白处,找到 `laboratory` -> `host_node` 在动作列表中找到 `test_latency`
4. 设置"关联设备名称"为 `host_node`
![选择 host_node](image/test_latency_select_device.png)
5. 点击保存,然后运行工作流
![运行测试](image/test_latency_running.png)
6. 在控制台查看执行结果,点击查看返回值
![查看测试结果](image/test_latency_result.png)
**测试结果解读:**
系统会进行 5 次 ping-pong 测试,返回 JSON 格式的结果,包含以下关键指标:
- **avg_rtt_ms**:平均往返延迟(毫秒),正常应在几十到几百毫秒
- **avg_time_diff_ms**:主机与云端的平均时间差(毫秒)
- **max_time_error_ms**:最大时间误差(毫秒)
- **raw_delay_ms**:原始延迟测量值(毫秒)
- **task_delay_ms**:任务处理延迟(毫秒)
- **test_count**:测试次数
- **status**测试状态success 表示成功)
**实际返回结果示例:**
```json
{
"error": "",
"return_value": {
"avg_rtt_ms": 49.64,
"avg_time_diff_ms": 3872.43,
"max_time_error_ms": 3885.34,
"raw_delay_ms": 3900.24,
"status": "success",
"task_delay_ms": 27.8,
"test_count": 5
},
"suc": true
}
```
**结果评估:**
- `avg_rtt_ms < 100ms`:网络连接良好
- `avg_rtt_ms 100-500ms`:网络可用,略有延迟
- `avg_rtt_ms > 500ms`:网络延迟较高,建议检查网络配置
- `status: "success"`:测试通过
---
### 7. 运行您的第一个工作流
现在系统已经正常运行,让我们通过一个完整的案例来创建并执行第一个工作流。
#### 7.1 工作流场景
我们将实现一个简单的液体转移实验:
1. 在容器 1 中加入水
2. 通过传输泵将容器 1 中的水转移到容器 2
#### 7.2 添加所需的设备和物料
在创建工作流之前,我们需要先在"仪器耗材"模块中添加必要的设备和物料。
**所需设备和物料清单:**
- 仪器设备 `work_station` 中的 `workstation` 数量 x1
- 仪器设备 `virtual_device` 中的 `virtual_transfer_pump` 数量 x1
- 物料耗材 `container` 中的 `container` 数量 x2
**操作步骤:**
1. 访问 Web 界面,进入"仪器耗材"模块
2. 在"仪器设备"区域找到并添加上述设备
3. 在"物料耗材"区域找到并添加容器
![物料列表](image/material.png)
#### 7.3 建立设备和物料的关联关系
当我们添加设备时,仪器耗材模块的物料列表会实时更新。我们需要将设备和物料拖拽到 workstation 中并在画布上将它们连接起来,就像真实的设备操作一样。
**操作步骤:**
1. 将两个 `container` 拖拽到 `workstation`
2.`virtual_transfer_pump` 拖拽到 `workstation`
3. 在画布上连接它们(建立父子关系)
![设备连接](image/links.png)
**连接规则:**
- 所有设备和物料都必须有父节点(除了顶层工作站)
- 连接关系反映真实的物理布局
- 连接后才能在工作流中使用
#### 7.4 创建工作流
1. 进入"工作流"模块
2. 点击"我创建的"
3. 点击"新建工作流"
![新建工作流](image/new.png)
#### 7.5 新增工作流节点
我们可以进入指定工作流,在空白处右键,添加以下节点:
- 选择 **Laboratory → host_node** 中的 `creat_resource`
- 选择 **Laboratory → workstation** 中的 `PumpTransferProtocol`
![创建工作流](image/creatworkfollow.gif)
#### 7.6 配置节点参数
根据案例,工作流包含两个步骤:
1. 使用 `creat_resource` 在容器中创建水
2. 通过泵传输协议将水传输到另一个容器
**配置 creat_resource 节点:**
点击 `creat_resource` 卡片上的编辑按钮,配置参数:
```
class_name: container
device_id: workstation
liquid_input_slot: 0 或 -1 均可
liquid_type: water
liquid_volume: 50根据需求填写默认单位 ml这里举例 50
parent: workstation
res_id: container
关联设备名称(原 unilabos_device_id): host_node
```
配置完成后点击底部保存按钮。
**配置 PumpTransferProtocol 节点:**
点击 `PumpTransferProtocol` 卡片上的编辑按钮,配置参数:
```
event: transfer_liquid
from_vessel: water
to_vessel: container1
volume: 50根据需求填写默认单位 ml这里举例 50
关联设备名称(原 unilabos_device_id): workstation
```
配置完成后点击底部保存按钮。
#### 7.7 运行工作流
1. 连接两个节点卡片creat_resource → PumpTransferProtocol
2. 点击底部保存按钮
3. 点击运行按钮执行工作流
![连接并运行](image/linksandrun.png)
#### 7.8 运行监控
- 运行状态和消息实时显示在底部控制台
- 如有报错,可点击查看详细信息
- 查看日志文件:`unilabos_data/logs/unilab.log`
#### 7.9 结果验证
工作流完成后,返回"仪器耗材"模块:
1. 点击 `container1` 卡片查看详情
2. 确认其中包含参数指定的水和容量50ml 的水)
至此,您已经成功运行了第一个工作流。
---
## 第二部分:进阶部署
### 8. 多节点部署Slave 模式)
详细的组网部署和主从模式配置请参考 [组网部署与主从模式配置](../developer_guide/networking_overview.md)。
#### 8.1 主从模式概述
**适用场景:**
- 设备物理位置分散在不同房间
- 需要独立的故障隔离域
- 逐步扩展实验室规模
- 分布式实验操作
**架构:**
```
主节点Host
├── 本地设备A
├── 本地设备B
└── ROS2 通信 ←→ 从节点Slave
├── 远程设备C
└── 远程设备D
```
#### 8.2 准备工作
**主节点准备:**
1. 确保主节点已正常启动
2. 记录主节点的 IP 地址
3. 确保网络互通(同一局域网或 VPN
**网络要求:**
- 主从节点在同一局域网
- 可以互相 ping 通
- 确保 ROS2 通信端口畅通
**验证网络连通性:**
在配置完成后,使用 ROS2 自带的 demo 节点来验证跨节点通信是否正常:
**在主节点机器上**(激活 unilab 环境后):
```bash
# 终端1启动 talker
ros2 run demo_nodes_cpp talker
# 终端2启动 listener
ros2 run demo_nodes_cpp listener
```
**在从节点机器上**(激活 unilab 环境后):
```bash
# 终端1启动 talker
ros2 run demo_nodes_cpp talker
# 终端2启动 listener
ros2 run demo_nodes_cpp listener
```
**注意**:必须在两台机器上**互相启动** talker 和 listener否则可能出现只能收不能发的单向通信问题。
**预期结果:**
- 每台机器的 listener 应该能同时接收到本地和远程 talker 发送的消息
- 如果只能看到本地消息,说明网络配置有问题
- 如果两台机器都能互相收发消息,则组网配置正确
**如果验证失败,尝试关闭防火墙:**
为了确保 ROS2 DDS 通信正常建议直接关闭防火墙而不是配置特定端口。ROS2 使用动态端口范围,配置特定端口可能导致通信问题。
**Linux**
```bash
# 关闭防火墙
sudo ufw disable
# 或者临时停止防火墙
sudo systemctl stop ufw
```
**Windows**
```powershell
# 在 Windows 安全中心关闭防火墙
# 控制面板 -> 系统和安全 -> Windows Defender 防火墙 -> 启用或关闭 Windows Defender 防火墙
```
#### 8.3 启动主节点
```bash
# 在主机Host上启动
conda activate unilab
unilab --ak your_ak --sk your_sk -g host.json --upload_registry
```
**主节点职责:**
- 与云端通信
- 管理全局资源
- 协调所有节点
#### 8.4 启动从节点
在另一台机器上:
```bash
# 在从机Slave上启动
conda activate unilab
unilab --ak your_ak --sk your_sk -g slave.json --is_slave
```
**从节点职责:**
- 管理本地设备
- 向主节点注册
- 执行分配的任务
- 不直接与云端通信
**重要:** 从节点使用相同的 AK/SK因为它们属于同一个实验室。
#### 8.5 判断连接成功
**方法 1检查 ROS2 节点列表**
在任一节点上运行:
```bash
ros2 node list
```
应该看到来自两个节点的设备:
```
/devices/host_node
/devices/workstation
/devices/liquid_handler_1 # 从节点的设备
```
**方法 2查看主节点设备列表**
在主节点的 Web 界面:
1. 进入"仪器耗材"模块
2. 查看设备列表
3. 从节点的设备应显示为"在线"状态
4. 设备卡片上会显示来源机器名称
**方法 3查看日志**
主节点日志会显示:
```
[Host Node] Node info update request received
[Host Node] Slave node registered: liquid_handler_1 from Machine_B
```
从节点日志会显示:
```
Slave node info updated.
Resource tree synchronized with host.
```
#### 8.6 注册机制说明
从节点启动时会自动向主节点注册:
**注册流程(代码逻辑):**
1. **从节点启动**`base_device_node.py``register_device` 方法):
```python
# 检测到非 Host 模式
if not BasicConfig.is_host_mode:
# 创建服务客户端
sclient = self.create_client(SerialCommand, "/node_info_update")
# 发送注册信息
self.send_slave_node_info(sclient)
```
2. **发送注册信息**`main_slave_run.py`
```python
request.command = json.dumps({
"machine_name": BasicConfig.machine_name,
"type": "slave",
"devices_config": devices_config.dump(),
"registry_config": registry_config,
})
```
3. **主节点接收**`host_node.py` 的 `_node_info_update_callback` 方法):
```python
def _node_info_update_callback(self, request, response):
info = json.loads(request.command)
machine_name = info["machine_name"]
edge_device_id = info["edge_device_id"]
# 记录设备来源机器
self.device_machine_names[edge_device_id] = machine_name
```
**关键点:**
- 注册是自动的,无需手动配置
- 主节点会记录每个设备来自哪台机器
- 从节点需要等待主节点的服务可用
#### 8.7 常见问题
**问题 1从节点启动卡住**
```
Waiting for host service...
```
**解决方案:**
- 检查主节点是否已启动
- 验证网络连通性
- 使用 `--slave_no_host` 参数跳过等待(仅用于测试)
**问题 2节点看不到彼此**
**解决方案:**
- 确保在同一网络
- 关闭防火墙
- 设置相同的 `ROS_DOMAIN_ID`(可选)
**问题 3注册失败**
**解决方案:**
- 检查 AK/SK 是否正确
- 查看主节点日志
- 重启从节点
---
## 第三部分:自定义开发
### 9. 创建您的第一个自定义设备
详细的设备驱动编写指南请参考 [添加设备驱动](../developer_guide/add_device.md)。
#### 9.1 为什么需要自定义设备?
Uni-Lab-OS 内置了常见设备,但您的实验室可能有特殊设备需要集成:
- 自研仪器
- 定制化改造的商业设备
- 特殊的实验流程
- 第三方设备集成
#### 9.2 创建 Python 包
为了方便开发和管理,建议为您的实验室创建独立的 Python 包。
**包结构:**
```
my_lab_devices/
├── setup.py
├── README.md
├── my_lab_devices/
│ ├── __init__.py
│ ├── devices/
│ │ ├── __init__.py
│ │ ├── my_pump.py
│ │ └── my_sensor.py
│ └── registry/
│ ├── devices/
│ │ ├── my_pump.yaml
│ │ └── my_sensor.yaml
│ └── resources/
│ └── my_container.yaml
```
**创建步骤:**
```bash
# 1. 创建包目录
mkdir -p my_lab_devices/my_lab_devices/devices
mkdir -p my_lab_devices/my_lab_devices/registry/devices
mkdir -p my_lab_devices/my_lab_devices/registry/resources
# 2. 创建 __init__.py
touch my_lab_devices/my_lab_devices/__init__.py
touch my_lab_devices/my_lab_devices/devices/__init__.py
```
#### 9.3 创建 setup.py
```python
# my_lab_devices/setup.py
from setuptools import setup, find_packages
setup(
name="my_lab_devices",
version="0.1.0",
description="My Laboratory Custom Devices",
author="Your Name",
author_email="your.email@example.com",
packages=find_packages(),
install_requires=[
"unilabos", # 依赖 Uni-Lab-OS
# 添加其他依赖
# "pyserial",
# "requests",
],
python_requires=">=3.11",
# 包含注册表文件
package_data={
"my_lab_devices": [
"registry/devices/*.yaml",
"registry/resources/*.yaml",
],
},
)
```
#### 9.4 开发安装
使用 `-e` 参数进行可编辑安装,这样代码修改后立即生效:
```bash
cd my_lab_devices
pip install -e . -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
```
**优势:**
- 代码修改实时生效,无需重新安装
- 方便调试和测试
- 支持版本控制git
#### 9.5 编写设备驱动
创建设备驱动文件:
```python
# my_lab_devices/my_lab_devices/devices/my_pump.py
from typing import Dict, Any
from unilabos.registry.placeholder_type import ResourceSlot
class MyPump:
"""自定义注射泵驱动
支持功能:
- 注射液体
- 抽取液体
- 速率控制
"""
def __init__(self, config: Dict[str, Any]):
"""初始化设备
Args:
config: 配置字典,包含端口、波特率等
"""
self.port = config.get('port', 'COM1')
self.baudrate = config.get('baudrate', 9600)
self._status = "idle"
self._current_volume = 0.0
# 初始化串口连接
# self.serial = serial.Serial(self.port, self.baudrate)
print(f"MyPump initialized on {self.port}")
@property
def status(self) -> str:
"""设备状态"""
return self._status
@property
def current_volume(self) -> float:
"""当前注射器内液体体积ml"""
return self._current_volume
def infuse(
self,
target: ResourceSlot,
volume: float,
rate: float = 1.0
) -> Dict[str, Any]:
"""注射液体到目标容器
Args:
target: 目标容器(前端显示资源选择器)
volume: 注射体积ml
rate: 注射速率ml/min
Returns:
执行结果
"""
self._status = "infusing"
print(f"Infusing {volume}ml to {target.name} at {rate}ml/min")
# 发送命令到硬件
# self.serial.write(f"INF {volume} {rate}\r\n".encode())
# 模拟执行
import time
time.sleep(volume / rate * 60) # 模拟注射时间
self._current_volume -= volume
self._status = "idle"
return {
"success": True,
"volume_infused": volume,
"target": target.id,
"message": f"Successfully infused {volume}ml"
}
def withdraw(
self,
source: ResourceSlot,
volume: float,
rate: float = 1.0
) -> Dict[str, Any]:
"""从源容器抽取液体
Args:
source: 源容器(前端显示资源选择器)
volume: 抽取体积ml
rate: 抽取速率ml/min
Returns:
执行结果
"""
self._status = "withdrawing"
print(f"Withdrawing {volume}ml from {source.name} at {rate}ml/min")
# 发送命令到硬件
# self.serial.write(f"WDR {volume} {rate}\r\n".encode())
# 模拟执行
import time
time.sleep(volume / rate * 60)
self._current_volume += volume
self._status = "idle"
return {
"success": True,
"volume_withdrawn": volume,
"source": source.id,
"message": f"Successfully withdrawn {volume}ml"
}
def set_rate(self, rate: float) -> Dict[str, Any]:
"""设置流速
Args:
rate: 流速ml/min
"""
print(f"Setting rate to {rate}ml/min")
# self.serial.write(f"RAT {rate}\r\n".encode())
return {
"success": True,
"rate": rate,
"message": f"Rate set to {rate}ml/min"
}
```
**关键点:**
- **类型注解**:使用 `ResourceSlot` 等特殊类型
- **@property**:状态属性会自动广播
- **返回 Dict**:所有动作方法返回字典类型
- **文档字符串**:详细说明参数和功能
#### 9.6 测试设备驱动
创建简单的测试脚本:
```python
# test_my_pump.py
from my_lab_devices.devices.my_pump import MyPump
# 创建设备实例
config = {
"port": "COM1",
"baudrate": 9600
}
pump = MyPump(config)
# 测试状态属性
print(f"Status: {pump.status}")
print(f"Current volume: {pump.current_volume}ml")
# 模拟注射(不使用 ResourceSlot
class MockResource:
id = "test_container"
name = "测试容器"
result = pump.infuse(MockResource(), volume=5.0, rate=2.0)
print(f"Result: {result}")
```
运行测试:
```bash
python test_my_pump.py
```
---
### 10. 生成和完善设备注册表
注册表Registry是 Uni-Lab-OS 的核心配置文件,定义了设备的动作、状态、参数等信息。系统提供了自动生成功能,帮助您快速完成基础配置。
> 完整的注册表编写、字段说明、高级配置等内容,请参考 [添加设备注册表](../developer_guide/add_registry.md)。
#### 10.1 注册表目录结构
Uni-Lab-OS 使用三类注册表:
```
registry/
├── devices/ # 设备注册表(仪器设备,如泵、加热器、机械臂等)
├── device_comms/ # 通信驱动注册表(如 Modbus、OPC UA
└── resources/ # 物料注册表(耗材、容器,如孔板、试管、试剂等)
```
#### 10.2 生成注册表(两种方式)
**方式 1使用 Web 界面注册表编辑器(推荐)**
1. 访问 Web 界面,进入"注册表编辑器"
2. 上传您的 Python 设备驱动文件
3. 点击"分析文件",系统自动生成注册表
4. 填写描述、图标等元数据
5. 下载生成的 YAML 文件到您的注册表目录
**方式 2使用 `--complete_registry` 参数(命令行)**
```bash
unilab --ak your_ak --sk your_sk -g graph.json \
--registry_path ./my_lab_devices/registry \
--complete_registry
```
系统会自动:
- 扫描 Python 设备类
- 分析方法签名和类型注解
- 识别 `ResourceSlot`、`DeviceSlot` 等特殊类型
- 生成完整的 YAML 注册表文件
#### 10.3 生成的注册表示例
自动生成的注册表大致如下(已简化):
```yaml
my_pump:
class:
module: my_lab_devices.devices.my_pump:MyPump
type: python
status_types: # 自动提取的状态
status: str
current_volume: float
action_value_mappings: # 自动生成的动作
infuse:
type: UniLabJsonCommand
goal:
target: target
volume: volume
placeholder_keys:
target: unilabos_resources # 自动识别 ResourceSlot
result:
success: success
description: '自定义注射泵驱动'
version: '1.0.0'
```
系统会自动识别特殊类型并生成前端选择器:
| Python 类型 | 前端效果 |
| -------------------- | -------------- |
| `ResourceSlot` | 资源单选下拉框 |
| `List[ResourceSlot]` | 资源多选下拉框 |
| `DeviceSlot` | 设备单选下拉框 |
| `List[DeviceSlot]` | 设备多选下拉框 |
#### 10.4 使用自定义注册表
启动时指定注册表路径:
```bash
unilab --ak your_ak --sk your_sk -g graph.json \
--registry_path ./my_lab_devices/registry \
--upload_registry
```
**支持多个注册表路径**(按顺序查找):
```bash
unilab --ak your_ak --sk your_sk -g graph.json \
--registry_path ./my_lab_devices/registry \
--registry_path ./another_lab/registry
```
#### 10.5 验证注册表
**验证方法:**
1. **查看启动日志**:确认设备被正确加载
2. **访问 Web 界面**"仪器设备"模块中应能看到您的自定义设备
3. **测试动作调用**:在工作流中测试设备动作是否正常
#### 10.6 手动完善注册表(可选)
自动生成的注册表已经可用,但您可以手动添加以下内容来提升用户体验:
- **description**:设备详细描述
- **icon**:设备图标(支持 Base64 或 URL
- **category**:设备分类标签(用于前端分组显示)
- **handles**:设备连接点定义(用于可视化连接)
**示例:**
```yaml
my_pump:
description: '高精度注射泵,支持 0.001-100 ml/min 流速范围'
icon: 'pump_icon.webp'
category:
- pump
- liquid_handling
# ... 其他配置
```
> 关于注册表的高级配置、字段详解、最佳实践等,请参考 [添加设备注册表](../developer_guide/add_registry.md)。
---
### 11. 创建复杂工作站
详细的工作站架构请参考 [工作站架构](../developer_guide/examples/workstation_architecture.md)。
#### 11.1 什么是复杂工作站?
复杂工作站是多个设备和物料的集合,协同完成特定实验流程。
**示例场景:**
- **液体处理工作站**:移液器 + 孔板 + 试剂槽 + 枪头架
- **有机合成工作站**:反应釜 + 注射泵 + 加热搅拌器 + 冷凝器
- **分析工作站**:色谱仪 + 进样器 + 流动相泵
#### 11.2 设计工作站
**设计原则:**
1. **模块化**:每个设备独立,可单独控制
2. **层次化**:使用父子关系组织设备
3. **标准化**:使用统一的接口和命名
**工作站结构:**
```
Workstation工作站根节点
├── Deck甲板/平台)
│ ├── PlateCarrier_1板位
│ │ └── Plate_9696孔板
│ ├── PlateCarrier_2
│ │ └── Plate_384384孔板
│ └── TipRack枪头架
├── Pipette_88通道移液器
└── WashStation清洗站
```
#### 11.3 组合多个设备
在组态图中:
1. 创建工作站根节点(通常是 `workstation`
2. 添加子设备
3. 建立连接关系
4. 配置每个设备的参数
**JSON 组态示例:**
```json
{
"nodes": [
{
"id": "liquid_handler_ws",
"name": "液体处理工作站",
"type": "device",
"class": "workstation",
"config": {}
},
{
"id": "pipette_8ch",
"name": "8通道移液器",
"type": "device",
"class": "pipette_8_channel",
"config": {
"volume_range": [0.5, 300]
}
},
{
"id": "plate_carrier_1",
"name": "板位1",
"type": "resource",
"class": "plate_carrier",
"config": {}
}
],
"links": [
{
"source": "liquid_handler_ws",
"target": "pipette_8ch",
"type": "contains"
},
{
"source": "liquid_handler_ws",
"target": "plate_carrier_1",
"type": "contains"
}
]
}
```
#### 11.4 设计工作流程
**复杂工作站的工作流特点:**
- 多步骤协同
- 设备间依赖
- 状态管理
- 错误恢复
**示例96 孔板液体分配流程**
```
1. 准备阶段
├── 检查枪头架
├── 检查孔板位置
└── 检查试剂体积
2. 执行阶段
├── 装载枪头
├── 从试剂槽吸取
├── 移动到目标孔板
├── 分配液体
└── 退出枪头
3. 清理阶段
├── 清洗移液器
└── 复位到初始位置
```
**在 Uni-Lab 中实现:**
1. 创建工作流
2. 添加各步骤节点
3. 配置每个节点的设备和参数
4. 连接节点形成流程
5. 添加错误处理分支
#### 11.5 通信共享机制
在复杂工作站中多个设备可能需要共享同一个通信端口如串口、PLC但大多数通信端口只能被一个实例占用。通信共享机制解决了这个问题允许灵活控制连接到同一通信端口的多个设备。
##### 11.5.1 应用场景
**典型场景:**
- **多泵阀共享 PLC 串口**:所有泵阀通过同一个 PLC 控制,泵阀数量可灵活增减
- **多传感器共享 Modbus**:多个传感器通过同一个 Modbus 设备读取数据
- **多设备共享 TCP/IP**:多台设备共享一个 TCP/IP 通信接口
**解决思路:**
创建一个**通信设备作为代理**,所有需要使用该端口的设备通过代理进行通信,而不是直接访问端口。
##### 11.5.2 配置步骤
**步骤 1定义 workstation 并指定 protocol_type**
在组态图中workstation 的 `config` 需要添加 `protocol_type`(协议类型列表,可为空):
```json
{
"id": "simple_station",
"name": "简单工作站",
"type": "device",
"class": "workstation",
"parent": null,
"config": {
"protocol_type": ["PumpTransferProtocol", "CleanProtocol"]
},
"children": ["serial_pump", "pump_1", "pump_2", "pump_3"]
}
```
> **说明**`children` 列表中,**通信设备必须排在最前面**(如 `"serial_pump"`),确保它先于其他设备初始化。
**步骤 2创建通信设备作为代理**
在 workstation 的子设备中,**首先定义通信设备**(必须在其他设备之前初始化,因此放到启动 json 靠前的位置):
```json
{
"id": "serial_pump",
"type": "device",
"class": "serial",
"parent": "simple_station",
"config": {
"port": "COM7",
"baudrate": 9600
}
}
```
**步骤 3配置使用该通信的设备**
其他设备的 `config` 中指向通信设备(字段名根据设备注册表定义,如 `port` 或 `io_device_port`,他们在驱动中):
```json
{
"id": "pump_1",
"type": "device",
"class": "syringe_pump_with_valve.runze.SY03B-T06",
"parent": "simple_station",
"config": {
"port": "serial_pump", // 直接使用通信设备的设备名最终能够通过__init__实现self.hardware_interface变量设定为serial_pump的字符串
"address": "1",
"max_volume": 25.0
}
}
```
> 配置字段的值直接是**通信设备的设备名**(如 `"serial_pump"`),不是完整路径。系统会自动进行变量替换。
**步骤 4建立通信连接关系**
在组态图的 `links` 中,建立 `communication` 类型的连接:
```json
{
"links": [
{
"source": "pump_1",
"target": "serial_pump",
"type": "communication",
"port": {
"serial_pump": "1" // 通信端口号或地址
}
},
{
"source": "pump_2",
"target": "serial_pump",
"type": "communication",
"port": {
"serial_pump": "2"
}
}
]
}
```
> **说明**`port` 字段中的值是通信设备上的端口号或地址,具体含义取决于通信协议。
##### 11.5.3 注册表配置
**通信设备的注册表**(例如`device_comms/modbus_ioboard.yaml`
```yaml
io_snrd:
class:
module: unilabos.device_comms.SRND_16_IO:SRND_16_IO
type: python
# hardware_interface.name 对象是实际的通信实例(如 ModbusClient()
# 通过 Python 的 __init__ 方法初始化
hardware_interface:
name: modbus_client
read: read_io_coil
write: write_io_coil
description: 'IO Board with 16 IOs via Modbus'
```
**使用通信的设备注册表**`devices/pump_and_valve.yaml`
```yaml
syringe_pump_with_valve.runze.SY03B-T06:
class:
module: unilabos.devices.pump_and_valve.runze_backbone:RunzeSyringePump
type: python
# hardware_interface.name 初始化时是通信设备的设备名(字符串)
# 通过启动 JSON 的 config 传入(如 "port": "serial_pump"
# 系统会自动将字符串替换为实际的通信实例
hardware_interface:
name: hardware_interface
read: send_command
write: send_command
action_value_mappings:
# 设备的动作定义
solenoid_valve:
class:
module: unilabos.devices.pump_and_valve.solenoid_valve:SolenoidValve
type: python
# 使用 io_device_port 字段接收通信设备名
hardware_interface:
name: io_device_port # 例如当前电磁阀修改了name从默认的hardware_interface改成了io_device_port那么启动json中就应该对__init__中的io_device_port进行赋值
read: read_io_coil
write: write_io_coil
```
**关键机制:**
| 设备类型 | hardware_interface.name | 初始化方式 | 运行时对象 |
| ------------------ | ----------------------- | --------------------------------------------- | ----------------------------------- |
| **通信设备** | `modbus_client` 等 | Python `__init__` 中创建实例 | `ModbusClient()`、`serial.Serial()` |
| **使用通信的设备** | `hardware_interface` 等 | config 中传入设备名字符串(如 `"io_device"` | **系统自动替换**为实际通信实例 |
**自动替换流程:**
1. 设备初始化时,`self.hardware_interface = "io_device"`(字符串)
2. workstation 检测到 `config` 中有通信设备名
3. 系统自动将 `self.hardware_interface` 替换为 `io_device` 的实际通信实例
4. 设备可以直接调用 `self.hardware_interface.read_io_coil()` 等方法
**完整示例Modbus IO 板控制多个电磁阀**
```json
{
"nodes": [
{
"id": "WorkStationTestMinimumSystem",
"name": "工作站节点",
"children": [
"io_device",
"Product1_Solenoid_Valve",
"Water_Solenoid_Valve"
],
"parent": null,
"type": "device",
"class": "workstation",
"config": {
"protocol_type": []
}
},
{
"id": "io_device",
"class": "io_snrd", // 通信设备类型
"name": "io_device",
"config": {
"address": "192.168.1.7:23", // Modbus TCP 地址
"slave_id": "1"
},
"parent": "WorkStationTestMinimumSystem",
"type": "device"
},
{
"id": "Product1_Solenoid_Valve",
"class": "solenoid_valve",
"name": "Product1_Solenoid_Valve",
"config": {
"io_device_port": "io_device" // 通信设备的设备名(字符串)
},
"parent": "WorkStationTestMinimumSystem",
"type": "device"
},
{
"id": "Water_Solenoid_Valve",
"class": "solenoid_valve",
"name": "Water_Solenoid_Valve",
"config": {
"io_device_port": "io_device" // 同样使用这个通信设备
},
"parent": "WorkStationTestMinimumSystem",
"type": "device"
}
],
"links": []
}
```
**工作原理:**
1. `io_device` 初始化时创建 `ModbusClient("192.168.1.7:23")`
2. 两个电磁阀的 `self.io_device_port` 初始化为字符串 `"io_device"`
3. workstation 自动将 `self.io_device_port` 替换为实际的 `ModbusClient` 实例
4. 电磁阀可以通过 `self.io_device_port.read_io_coil(15)` 或 `write_io_coil(14)` 操作
##### 11.5.4 验证通信共享
**启动成功标志:**
系统启动时,在日志中查看以下提示信息:
```
通信代理:为子设备 pump_1 配置通信代理 serial_pump
通信代理:为子设备 pump_2 配置通信代理 serial_pump
通信代理:为子设备 pump_3 配置通信代理 serial_pump
```
看到此提示说明通信共享配置成功。
**测试步骤:**
1. 启动系统,查看日志确认通信代理配置成功
2. 在 Web 界面中,分别控制 `pump_1`、`pump_2`、`pump_3`
3. 确认所有泵都能正常响应(通过同一个串口)
4. 查看通信设备的状态,确认消息正常传递
---
### 12. 撰写物料定义
详细的物料编写指南请参考:
- [物料构建指南](../developer_guide/examples/materials_construction_guide.md)
- [物料教程](../developer_guide/examples/materials_tutorial.md)
#### 12.1 物料Resource概述
物料是实验中使用的消耗品和容器:
- **容器**:孔板、试管、烧杯、反应瓶
- **耗材**:枪头、移液管、过滤器
- **试剂**:溶剂、试剂、样品
#### 12.2 创建自定义物料类型
**场景:** 创建实验室特殊规格的孔板
```python
# my_lab_devices/my_lab_devices/resources/custom_plate.py
from pylabrobot.resources import Plate, Well
from typing import List
class CustomPlate48(Plate):
"""自定义 48 孔板
规格:
- 6行 x 8列
- 孔体积2ml
- 孔间距18mm
"""
def __init__(self, name: str):
super().__init__(
name=name,
size_x=127.76, # mm
size_y=85.48, # mm
size_z=14.35, # mm
model="custom_48_wellplate_2000ul"
)
# 定义孔位
self._create_wells()
def _create_wells(self):
"""创建 48 个孔位"""
rows = "ABCDEF"
cols = range(1, 9)
well_size_x = 16.0 # mm
well_size_y = 16.0
well_depth = 12.0
spacing_x = 18.0
spacing_y = 18.0
for row_idx, row in enumerate(rows):
for col in cols:
well_name = f"{row}{col}"
x = (col - 1) * spacing_x + 10 # 10mm 边距
y = row_idx * spacing_y + 10
well = Well(
name=well_name,
size_x=well_size_x,
size_y=well_size_y,
size_z=well_depth,
max_volume=2000, # μL
)
well.location = (x, y, 0)
self.children.append(well)
```
#### 12.3 物料注册表
创建物料注册表:
```yaml
# my_lab_devices/my_lab_devices/registry/resources/custom_plate.yaml
custom_48_wellplate_2000ul:
class:
module: my_lab_devices.resources.custom_plate:CustomPlate48
type: python
description: '自定义 48 孔板2ml 孔体积'
version: '1.0.0'
category:
- plate
- container
# 物料属性
properties:
rows: 6
columns: 8
well_count: 48
well_volume: 2000 # μL
well_shape: 'round'
material: 'polystyrene'
# 尺寸信息
dimensions:
size_x: 127.76
size_y: 85.48
size_z: 14.35
unit: 'mm'
# 兼容性
compatible_with:
- 'plate_carrier'
- 'thermal_cycler'
icon: ''
```
#### 12.4 物料属性定义
**常见属性:**
```yaml
properties:
# 容量相关
max_volume: 2000 # μL
min_volume: 10 # μL
dead_volume: 5 # μL无法吸取的残留体积
# 几何形状
shape: 'round' # round, square, v_bottom
diameter: 16.0 # mm
depth: 12.0 # mm
# 材质
material: 'polystyrene' # 聚苯乙烯
transparency: true # 透明
sterile: false # 无菌
# 温度范围
min_temperature: -80 # °C
max_temperature: 121 # °C
# 化学兼容性
chemical_compatibility:
- 'water'
- 'ethanol'
- 'dmso'
# 条形码
barcode_support: true
barcode_type: '1D'
```
#### 12.5 在系统中使用自定义物料
1. **注册物料**
```bash
unilab --ak your_ak --sk your_sk -g graph.json \
--registry_path ./my_lab_devices/my_lab_devices/registry \
--upload_registry
```
2. **在组态图中添加**
在 Web 界面的"物料耗材"模块中,找到 `custom_48_wellplate_2000ul`,添加到实验室。
3. **在工作流中使用**
工作流节点的参数中,涉及容器选择的地方会显示您的自定义物料。
---
## 第四部分:最佳实践总结
### 13. 开发流程建议
```
需求分析 → 环境搭建 → 原型验证 → 迭代开发 → 测试部署 → 生产运行
```
**详细步骤:**
1. **需求分析**
- 明确实验流程
- 列出所需设备和物料
- 设计工作流程图
2. **环境搭建**
- 安装 Uni-Lab-OS
- 创建实验室账号
- 准备开发工具IDE、Git
3. **原型验证**
- 使用虚拟设备测试流程
- 验证工作流逻辑
- 调整参数
4. **迭代开发**
- 实现自定义设备驱动(同时撰写单点函数测试)
- 编写注册表
- 单元测试
- 集成测试
5. **测试部署**
- 连接真实硬件
- 空跑测试
- 小规模试验
6. **生产运行**
- 全量部署
- 监控运行状态
- 持续优化
---
### 14. 进一步学习资源
#### 14.1 官方文档
- **安装指南**[installation.md](installation.md)
- **启动指南**[launch.md](launch.md)
- **配置指南**[../advanced_usage/configuration.md](../advanced_usage/configuration.md)
#### 14.2 开发者文档
- **组网部署与主从模式**[../developer_guide/networking_overview.md](../developer_guide/networking_overview.md)
- **添加设备驱动**[../developer_guide/add_device.md](../developer_guide/add_device.md)
- **老设备驱动开发**[../developer_guide/add_old_device.md](../developer_guide/add_old_device.md)
- **添加动作指令**[../developer_guide/add_action.md](../developer_guide/add_action.md)
- **YAML 注册表编写**[../developer_guide/add_yaml.md](../developer_guide/add_yaml.md)
- **添加设备注册表**[../developer_guide/add_registry.md](../developer_guide/add_registry.md)
- **工作站架构**[../developer_guide/examples/workstation_architecture.md](../developer_guide/examples/workstation_architecture.md)
- **物料构建指南**[../developer_guide/examples/materials_construction_guide.md](../developer_guide/examples/materials_construction_guide.md)
#### 14.3 进阶主题
- **工作目录详解**[../advanced_usage/working_directory.md](../advanced_usage/working_directory.md)
<!-- 以下文档待补充:
- **远程资源管理**[../deployment/03_remote_resources.md](../deployment/03_remote_resources.md)
- **云端集成**[../deployment/04_cloud_integration.md](../deployment/04_cloud_integration.md)
- **生产环境部署**[../deployment/05_production_deployment.md](../deployment/05_production_deployment.md)
-->
#### 14.4 外部资源
- **ROS 2 文档**[https://docs.ros.org/en/humble/](https://docs.ros.org/en/humble/)
- **PyLabRobot**[https://github.com/PyLabRobot/pylabrobot](https://github.com/PyLabRobot/pylabrobot)
- **Python 类型注解**[https://docs.python.org/3/library/typing.html](https://docs.python.org/3/library/typing.html)
- **YAML 语法**[https://yaml.org/](https://yaml.org/)
#### 14.5 社区支持
- **GitHub Issues**[https://github.com/dptech-corp/Uni-Lab-OS/issues](https://github.com/dptech-corp/Uni-Lab-OS/issues)
- **官方网站**[https://uni-lab.bohrium.com](https://uni-lab.bohrium.com)
---
## 结语
通过本指南,您应该已经掌握了:
- 安装和配置 Uni-Lab-OS 环境
- 创建和管理实验室
- 启动系统并运行工作流
- 部署多节点分布式系统
- 开发自定义设备驱动
- 创建和注册物料定义
- 构建复杂工作站
**下一步建议:**
1. 从简单的工作流开始实践,逐步尝试更复杂的场景
2. 在 GitHub 上提问和分享经验
3. 关注文档更新和新功能发布
4. 为 Uni-Lab-OS 社区贡献您的设备驱动和最佳实践
---
_本指南最后更新2025-11_
_Uni-Lab-OS 版本最新稳定版_