diff --git a/MANIFEST.in b/MANIFEST.in index aa1a5d87..d81945e6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ +recursive-include unilabos/test * recursive-include unilabos/registry *.yaml recursive-include unilabos/app/web/static * recursive-include unilabos/app/web/templates * diff --git a/docs/advanced_usage/configuration.md b/docs/advanced_usage/configuration.md new file mode 100644 index 00000000..d1fdb69e --- /dev/null +++ b/docs/advanced_usage/configuration.md @@ -0,0 +1,746 @@ +# Uni-Lab 配置指南 + +本文档详细介绍 Uni-Lab 配置文件的结构、配置项、命令行覆盖和环境变量的使用方法。 + +## 配置文件概述 + +Uni-Lab 使用 Python 格式的配置文件(`.py`),默认为 `unilabos_data/local_config.py`。配置文件采用类属性的方式定义各种配置项,比 YAML 或 JSON 提供更多的灵活性,包括支持注释、条件逻辑和复杂数据结构。 + +## 获取实验室密钥 + +在配置文件或启动命令中,您需要提供实验室的访问密钥(ak)和私钥(sk)。 + +**获取方式:** + +进入 [Uni-Lab 实验室](https://uni-lab.bohrium.com),点击左下角的头像,在实验室详情中获取所在实验室的 ak 和 sk: + +![copy_aksk.gif](image/copy_aksk.gif) + +## 配置文件格式 + +### 默认配置示例 + +首次使用时,系统会自动创建一个基础配置文件 `local_config.py`: + +```python +# unilabos的配置文件 + +class BasicConfig: + ak = "" # 实验室网页给您提供的ak代码 + sk = "" # 实验室网页给您提供的sk代码 + + +# WebSocket配置,一般无需调整 +class WSConfig: + reconnect_interval = 5 # 重连间隔(秒) + max_reconnect_attempts = 999 # 最大重连次数 + ping_interval = 30 # ping间隔(秒) +``` + +### 完整配置示例 + +您可以根据需要添加更多配置选项: + +```python +#!/usr/bin/env python +# coding=utf-8 +"""Uni-Lab 配置文件""" + +# 基础配置 +class BasicConfig: + ak = "" # 实验室访问密钥 + sk = "" # 实验室私钥 + working_dir = "" # 工作目录(通常自动设置) + config_path = "" # 配置文件路径(自动设置) + is_host_mode = True # 是否为主站模式 + slave_no_host = False # 从站模式下是否跳过等待主机服务 + upload_registry = False # 是否上传注册表 + machine_name = "undefined" # 机器名称(自动获取) + vis_2d_enable = False # 是否启用2D可视化 + enable_resource_load = True # 是否启用资源加载 + communication_protocol = "websocket" # 通信协议 + log_level = "DEBUG" # 日志级别:TRACE, DEBUG, INFO, WARNING, ERROR, CRITICAL + +# WebSocket配置 +class WSConfig: + reconnect_interval = 5 # 重连间隔(秒) + max_reconnect_attempts = 999 # 最大重连次数 + ping_interval = 30 # ping间隔(秒) + +# OSS上传配置 +class OSSUploadConfig: + api_host = "" # API主机地址 + authorization = "" # 授权信息 + init_endpoint = "" # 初始化端点 + complete_endpoint = "" # 完成端点 + max_retries = 3 # 最大重试次数 + +# HTTP配置 +class HTTPConfig: + remote_addr = "https://uni-lab.bohrium.com/api/v1" # 远程服务器地址 + +# ROS配置 +class ROSConfig: + modules = [ + "std_msgs.msg", + "geometry_msgs.msg", + "control_msgs.msg", + "control_msgs.action", + "nav2_msgs.action", + "unilabos_msgs.msg", + "unilabos_msgs.action", + ] # 需要加载的ROS模块 +``` + +## 配置优先级 + +配置项的生效优先级从高到低为: + +1. **命令行参数**:最高优先级 +2. **环境变量**:中等优先级 +3. **配置文件**:基础优先级 + +这意味着命令行参数会覆盖环境变量和配置文件,环境变量会覆盖配置文件。 + +## 推荐配置方式 + +根据参数特性,不同配置项有不同的推荐配置方式: + +### 建议通过命令行指定的参数(不需要写入配置文件) + +以下参数推荐通过命令行或环境变量指定,**一般不需要在配置文件中配置**: + +| 参数 | 命令行参数 | 原因 | +| ----------------- | ------------------- | ------------------------------------ | +| `ak` / `sk` | `--ak` / `--sk` | **安全考虑**:避免敏感信息泄露 | +| `working_dir` | `--working_dir` | **灵活性**:不同环境可能使用不同目录 | +| `is_host_mode` | `--is_slave` | **运行模式**:由启动场景决定,不固定 | +| `slave_no_host` | `--slave_no_host` | **运行模式**:从站特殊配置,按需使用 | +| `upload_registry` | `--upload_registry` | **临时操作**:仅首次启动或更新时需要 | +| `vis_2d_enable` | `--2d_vis` | **调试功能**:按需临时启用 | +| `remote_addr` | `--addr` | **环境切换**:测试/生产环境快速切换 | + +**推荐用法示例:** + +```bash +# 标准启动命令(所有必要参数通过命令行指定) +unilab --ak your_ak --sk your_sk -g graph.json + +# 测试环境 +unilab --addr test --ak your_ak --sk your_sk -g graph.json + +# 从站模式 +unilab --is_slave --ak your_ak --sk your_sk + +# 首次启动上传注册表 +unilab --ak your_ak --sk your_sk -g graph.json --upload_registry +``` + +### 适合在配置文件中配置的参数 + +以下参数适合在配置文件中配置,通常不会频繁更改: + +| 参数 | 配置类 | 说明 | +| ------------------------ | ----------- | ---------------------- | +| `log_level` | BasicConfig | 日志级别配置 | +| `reconnect_interval` | WSConfig | WebSocket 重连间隔 | +| `max_reconnect_attempts` | WSConfig | WebSocket 最大重连次数 | +| `ping_interval` | WSConfig | WebSocket 心跳间隔 | +| `modules` | ROSConfig | ROS 模块列表 | + +**配置文件示例(推荐最小配置):** + +```python +# unilabos的配置文件 + +class BasicConfig: + log_level = "INFO" # 生产环境建议 INFO,调试时用 DEBUG + +# WebSocket配置,一般保持默认即可 +class WSConfig: + reconnect_interval = 5 + max_reconnect_attempts = 999 + ping_interval = 30 +``` + +**注意:** `ak` 和 `sk` 不建议写在配置文件中,始终通过命令行参数或环境变量传递。 + +## 命令行参数覆盖配置 + +Uni-Lab 允许通过命令行参数覆盖配置文件中的设置,提供更灵活的配置方式。 + +### 支持命令行覆盖的配置项 + +| 配置类 | 配置字段 | 命令行参数 | 说明 | +| ------------- | ----------------- | ------------------- | -------------------------------- | +| `BasicConfig` | `ak` | `--ak` | 实验室访问密钥 | +| `BasicConfig` | `sk` | `--sk` | 实验室私钥 | +| `BasicConfig` | `working_dir` | `--working_dir` | 工作目录路径 | +| `BasicConfig` | `is_host_mode` | `--is_slave` | 主站模式(参数为从站模式,取反) | +| `BasicConfig` | `slave_no_host` | `--slave_no_host` | 从站模式下跳过等待主机服务 | +| `BasicConfig` | `upload_registry` | `--upload_registry` | 启动时上传注册表信息 | +| `BasicConfig` | `vis_2d_enable` | `--2d_vis` | 启用 2D 可视化 | +| `HTTPConfig` | `remote_addr` | `--addr` | 远程服务地址 | + +### 特殊命令行参数 + +除了直接覆盖配置项的参数外,还有一些特殊的命令行参数: + +| 参数 | 说明 | +| ------------------- | ------------------------------------ | +| `--config` | 指定配置文件路径 | +| `--port` | Web 服务端口(不影响配置文件) | +| `--disable_browser` | 禁用自动打开浏览器(不影响配置文件) | +| `--visual` | 可视化工具选择(不影响配置文件) | +| `--skip_env_check` | 跳过环境检查(不影响配置文件) | + +### 命令行覆盖使用示例 + +```bash +# 通过命令行覆盖认证信息 +unilab --ak "new_access_key" --sk "new_secret_key" -g graph.json + +# 覆盖服务器地址 +unilab --ak ak --sk sk --addr "https://custom.server.com/api/v1" -g graph.json + +# 启用从站模式并跳过等待主机 +unilab --is_slave --slave_no_host --ak ak --sk sk + +# 启用上传注册表和2D可视化 +unilab --upload_registry --2d_vis --ak ak --sk sk -g graph.json + +# 组合使用多个覆盖参数 +unilab --ak "key" --sk "secret" --addr "test" --upload_registry --2d_vis -g graph.json +``` + +### 预设环境地址 + +`--addr` 参数支持以下预设值,会自动转换为对应的完整 URL: + +- `test` → `https://uni-lab.test.bohrium.com/api/v1` +- `uat` → `https://uni-lab.uat.bohrium.com/api/v1` +- `local` → `http://127.0.0.1:48197/api/v1` +- 其他值 → 直接使用作为完整 URL + +## 配置选项详解 + +### 1. BasicConfig - 基础配置 + +基础配置包含了系统运行的核心参数: + +| 参数 | 类型 | 默认值 | 说明 | +| ------------------------ | ---- | ------------- | ------------------------------------------ | +| `ak` | str | `""` | 实验室访问密钥(必需) | +| `sk` | str | `""` | 实验室私钥(必需) | +| `working_dir` | str | `""` | 工作目录,通常自动设置 | +| `config_path` | str | `""` | 配置文件路径,自动设置 | +| `is_host_mode` | bool | `True` | 是否为主站模式 | +| `slave_no_host` | bool | `False` | 从站模式下是否跳过等待主机服务 | +| `upload_registry` | bool | `False` | 启动时是否上传注册表信息 | +| `machine_name` | str | `"undefined"` | 机器名称,自动从 hostname 获取(不可配置) | +| `vis_2d_enable` | bool | `False` | 是否启用 2D 可视化 | +| `enable_resource_load` | bool | `True` | 是否启用资源加载 | +| `communication_protocol` | str | `"websocket"` | 通信协议,固定为 websocket | +| `log_level` | str | `"DEBUG"` | 日志级别 | + +#### 日志级别选项 + +- `TRACE` - 追踪级别(最详细) +- `DEBUG` - 调试级别(默认) +- `INFO` - 信息级别 +- `WARNING` - 警告级别 +- `ERROR` - 错误级别 +- `CRITICAL` - 严重错误级别(最简略) + +#### 认证配置(ak / sk) + +`ak` 和 `sk` 是必需的认证参数: + +1. **获取方式**:在 [Uni-Lab 官网](https://uni-lab.bohrium.com) 注册实验室后获得 +2. **配置方式**: + - **命令行参数**:`--ak "your_key" --sk "your_secret"`(最高优先级,推荐) + - **环境变量**:`UNILABOS_BASICCONFIG_AK` 和 `UNILABOS_BASICCONFIG_SK` + - **配置文件**:在 `BasicConfig` 类中设置(不推荐,安全风险) +3. **安全注意**:请妥善保管您的密钥信息,不要提交到版本控制 + +**推荐做法**: + +- **开发环境**:使用命令行参数或环境变量 +- **生产环境**:使用环境变量 +- **临时测试**:使用命令行参数 + +### 2. WSConfig - WebSocket 配置 + +WebSocket 是 Uni-Lab 的主要通信方式: + +| 参数 | 类型 | 默认值 | 说明 | +| ------------------------ | ---- | ------ | ------------------ | +| `reconnect_interval` | int | `5` | 断线重连间隔(秒) | +| `max_reconnect_attempts` | int | `999` | 最大重连次数 | +| `ping_interval` | int | `30` | 心跳检测间隔(秒) | + +### 3. HTTPConfig - HTTP 配置 + +HTTP 客户端配置用于与云端服务通信: + +| 参数 | 类型 | 默认值 | 说明 | +| ------------- | ---- | -------------------------------------- | ------------ | +| `remote_addr` | str | `"https://uni-lab.bohrium.com/api/v1"` | 远程服务地址 | + +**预设环境地址**: + +- 生产环境:`https://uni-lab.bohrium.com/api/v1`(默认) +- 测试环境:`https://uni-lab.test.bohrium.com/api/v1` +- UAT 环境:`https://uni-lab.uat.bohrium.com/api/v1` +- 本地环境:`http://127.0.0.1:48197/api/v1` + +### 4. OSSUploadConfig - OSS 上传配置 + +对象存储服务配置,用于文件上传功能: + +| 参数 | 类型 | 默认值 | 说明 | +| ------------------- | ---- | ------ | -------------------- | +| `api_host` | str | `""` | OSS API 主机地址 | +| `authorization` | str | `""` | 授权认证信息 | +| `init_endpoint` | str | `""` | 上传初始化端点 | +| `complete_endpoint` | str | `""` | 上传完成端点 | +| `max_retries` | int | `3` | 上传失败最大重试次数 | + +### 5. ROSConfig - ROS 配置 + +配置 ROS 消息转换器需要加载的模块: + +| 配置项 | 类型 | 默认值 | 说明 | +| --------- | ---- | ---------- | ------------ | +| `modules` | list | 见下方示例 | ROS 模块列表 | + +**默认模块列表:** + +```python +class ROSConfig: + modules = [ + "std_msgs.msg", # 标准消息类型 + "geometry_msgs.msg", # 几何消息类型 + "control_msgs.msg", # 控制消息类型 + "control_msgs.action", # 控制动作类型 + "nav2_msgs.action", # 导航动作类型 + "unilabos_msgs.msg", # UniLab 自定义消息类型 + "unilabos_msgs.action", # UniLab 自定义动作类型 + ] +``` + +您可以根据实际使用的设备和功能添加其他 ROS 模块。 + +## 环境变量配置 + +Uni-Lab 支持通过环境变量覆盖配置文件中的设置。 + +### 环境变量命名规则 + +``` +UNILABOS_<配置类名>_<配置项名> +``` + +**注意:** + +- 环境变量名不区分大小写 +- 配置类名和配置项名都会转换为大写进行匹配 + +### 设置环境变量 + +#### Linux / macOS + +```bash +# 临时设置(当前终端) +export UNILABOS_BASICCONFIG_LOG_LEVEL=INFO +export UNILABOS_BASICCONFIG_AK="your_access_key" +export UNILABOS_BASICCONFIG_SK="your_secret_key" + +# 永久设置(添加到 ~/.bashrc 或 ~/.zshrc) +echo 'export UNILABOS_BASICCONFIG_LOG_LEVEL=INFO' >> ~/.bashrc +source ~/.bashrc +``` + +#### Windows (cmd) + +```cmd +# 临时设置 +set UNILABOS_BASICCONFIG_LOG_LEVEL=INFO +set UNILABOS_BASICCONFIG_AK=your_access_key + +# 永久设置(系统环境变量) +setx UNILABOS_BASICCONFIG_LOG_LEVEL INFO +``` + +#### Windows (PowerShell) + +```powershell +# 临时设置 +$env:UNILABOS_BASICCONFIG_LOG_LEVEL="INFO" +$env:UNILABOS_BASICCONFIG_AK="your_access_key" + +# 永久设置 +[Environment]::SetEnvironmentVariable("UNILABOS_BASICCONFIG_LOG_LEVEL", "INFO", "User") +``` + +### 环境变量类型转换 + +系统会根据配置项的原始类型自动转换环境变量值: + +| 原始类型 | 转换规则 | +| -------- | --------------------------------------- | +| `bool` | "true", "1", "yes" → True;其他 → False | +| `int` | 转换为整数 | +| `float` | 转换为浮点数 | +| `str` | 直接使用字符串值 | + +**示例:** + +```bash +# 布尔值 +export UNILABOS_BASICCONFIG_IS_HOST_MODE=true # 将设置为 True +export UNILABOS_BASICCONFIG_IS_HOST_MODE=false # 将设置为 False + +# 整数 +export UNILABOS_WSCONFIG_RECONNECT_INTERVAL=10 # 将设置为 10 + +# 字符串 +export UNILABOS_BASICCONFIG_LOG_LEVEL=INFO # 将设置为 "INFO" +``` + +### 环境变量示例 + +```bash +# 设置基础配置 +export UNILABOS_BASICCONFIG_AK="your_access_key" +export UNILABOS_BASICCONFIG_SK="your_secret_key" +export UNILABOS_BASICCONFIG_IS_HOST_MODE="true" + +# 设置WebSocket配置 +export UNILABOS_WSCONFIG_RECONNECT_INTERVAL="10" +export UNILABOS_WSCONFIG_MAX_RECONNECT_ATTEMPTS="500" + +# 设置HTTP配置 +export UNILABOS_HTTPCONFIG_REMOTE_ADDR="https://uni-lab.test.bohrium.com/api/v1" +``` + +## 配置文件使用方法 + +### 1. 使用默认配置文件(推荐) + +系统会自动查找并加载配置文件: + +```bash +# 直接启动,使用默认的 unilabos_data/local_config.py +unilab --ak your_ak --sk your_sk -g graph.json +``` + +查找顺序: + +1. 环境变量 `UNILABOS_BASICCONFIG_CONFIG_PATH` 指定的路径 +2. 工作目录下的 `local_config.py` +3. 首次使用时会引导创建配置文件 + +### 2. 指定配置文件启动 + +```bash +# 使用指定配置文件启动 +unilab --config /path/to/your/config.py --ak ak --sk sk -g graph.json +``` + +### 3. 配置文件验证 + +系统启动时会自动验证配置文件: + +- **语法检查**:确保 Python 语法正确 +- **类型检查**:验证配置项类型是否匹配 +- **加载确认**:控制台输出加载成功信息 + +## 常用配置场景 + +### 场景 1:调整日志级别 + +**配置文件方式:** + +```python +class BasicConfig: + log_level = "INFO" # 生产环境建议使用 INFO 或 WARNING +``` + +**环境变量方式:** + +```bash +export UNILABOS_BASICCONFIG_LOG_LEVEL=INFO +unilab --ak ak --sk sk -g graph.json +``` + +**命令行方式**(需要配置文件已包含): + +```bash +# 配置文件无直接命令行参数,需通过环境变量 +UNILABOS_BASICCONFIG_LOG_LEVEL=INFO unilab --ak ak --sk sk -g graph.json +``` + +### 场景 2:配置 WebSocket 重连 + +**配置文件方式:** + +```python +class WSConfig: + reconnect_interval = 10 # 增加重连间隔到 10 秒 + max_reconnect_attempts = 100 # 减少最大重连次数到 100 次 +``` + +**环境变量方式:** + +```bash +export UNILABOS_WSCONFIG_RECONNECT_INTERVAL=10 +export UNILABOS_WSCONFIG_MAX_RECONNECT_ATTEMPTS=100 +``` + +### 场景 3:切换服务器环境 + +**配置文件方式:** + +```python +class HTTPConfig: + remote_addr = "https://uni-lab.test.bohrium.com/api/v1" +``` + +**环境变量方式:** + +```bash +export UNILABOS_HTTPCONFIG_REMOTE_ADDR=https://uni-lab.test.bohrium.com/api/v1 +``` + +**命令行方式(推荐):** + +```bash +unilab --addr test --ak your_ak --sk your_sk -g graph.json +``` + +### 场景 4:从站模式配置 + +**配置文件方式:** + +```python +class BasicConfig: + is_host_mode = False # 从站模式 + slave_no_host = True # 不等待主机服务 +``` + +**命令行方式(推荐):** + +```bash +unilab --is_slave --slave_no_host --ak your_ak --sk your_sk +``` + +## 最佳实践 + +### 1. 安全配置 + +**不要在配置文件中存储敏感信息** + +- ❌ **不推荐**:在配置文件中明文存储 ak/sk +- ✅ **推荐**:使用环境变量或命令行参数 + +```bash +# 生产环境 - 使用环境变量(推荐) +export UNILABOS_BASICCONFIG_AK="your_access_key" +export UNILABOS_BASICCONFIG_SK="your_secret_key" +unilab -g graph.json + +# 或使用命令行参数 +unilab --ak "your_access_key" --sk "your_secret_key" -g graph.json +``` + +**其他安全建议:** + +- 不要将包含密钥的配置文件提交到版本控制系统 +- 限制配置文件权限:`chmod 600 local_config.py` +- 定期更换访问密钥 +- 使用 `.gitignore` 排除配置文件 + +### 2. 多环境配置 + +为不同环境创建不同的配置文件: + +``` +configs/ +├── base_config.py # 基础配置(非敏感) +├── dev_config.py # 开发环境 +├── test_config.py # 测试环境 +├── prod_config.py # 生产环境 +└── example_config.py # 示例配置 +``` + +**环境切换示例**: + +```bash +# 本地开发环境 +unilab --config configs/dev_config.py --addr local --ak ak --sk sk -g graph.json + +# 测试环境 +unilab --config configs/test_config.py --addr test --ak ak --sk sk --upload_registry -g graph.json + +# 生产环境 +unilab --config configs/prod_config.py --ak "$PROD_AK" --sk "$PROD_SK" -g graph.json +``` + +### 3. 配置管理 + +**配置文件最佳实践:** + +- 保持配置文件简洁,只包含需要修改的配置项 +- 为配置项添加注释说明其作用 +- 定期检查和更新配置文件 +- 版本控制仅保存示例配置,不包含实际密钥 + +**命令行参数优先使用场景:** + +- 临时测试不同配置 +- CI/CD 流水线中的动态配置 +- 不同环境间快速切换 +- 敏感信息的安全传递 + +### 4. 灵活配置策略 + +**基础配置文件 + 命令行覆盖**的推荐方式: + +```python +# base_config.py - 基础配置(非敏感信息) +class BasicConfig: + # 非敏感配置写在文件中 + is_host_mode = True + upload_registry = False + vis_2d_enable = False + log_level = "INFO" + +class WSConfig: + reconnect_interval = 5 + max_reconnect_attempts = 999 + ping_interval = 30 +``` + +```bash +# 启动时通过命令行覆盖关键参数 +unilab --config base_config.py \ + --ak "$AK" \ + --sk "$SK" \ + --addr "test" \ + --upload_registry \ + --2d_vis \ + -g graph.json +``` + +## 故障排除 + +### 1. 配置文件加载失败 + +**错误信息**:`[ENV] 配置文件 xxx 不存在` + +**解决方法**: + +- 确认配置文件路径正确 +- 检查文件权限是否可读 +- 确保配置文件是 `.py` 格式 +- 使用绝对路径或相对于当前目录的路径 + +### 2. 语法错误 + +**错误信息**:`[ENV] 加载配置文件 xxx 失败` + +**解决方法**: + +- 检查 Python 语法是否正确 +- 确认类名和字段名拼写正确 +- 验证缩进是否正确(使用空格而非制表符) +- 确保字符串使用引号包裹 + +### 3. 认证失败 + +**错误信息**:`后续运行必须拥有一个实验室` + +**解决方法**: + +- 确认 `ak` 和 `sk` 已正确配置 +- 检查密钥是否有效(未过期或撤销) +- 确认网络连接正常 +- 验证密钥是否来自正确的实验室 + +### 4. 环境变量不生效 + +**解决方法**: + +- 确认环境变量名格式正确(`UNILABOS_<类名>_<字段名>`) +- 检查环境变量是否已正确设置(`echo $VARIABLE_NAME`) +- 重启终端或重新加载环境变量 +- 确认环境变量值的类型正确 + +### 5. 命令行参数不生效 + +**错误现象**:设置了命令行参数但配置没有生效 + +**解决方法**: + +- 确认参数名拼写正确(如 `--ak` 而不是 `--access_key`) +- 检查参数格式是否正确(布尔参数如 `--is_slave` 不需要值) +- 确认参数位置正确(所有参数都应在 `unilab` 之后) +- 查看启动日志确认参数是否被正确解析 +- 检查是否有配置文件或环境变量与之冲突 + +### 6. 配置优先级混淆 + +**错误现象**:不确定哪个配置生效 + +**解决方法**: + +- 记住优先级:**命令行参数 > 环境变量 > 配置文件** +- 使用 `--ak` 和 `--sk` 参数时会看到提示信息:"传入了 ak 参数,优先采用传入参数!" +- 检查启动日志中的配置加载信息 +- 临时移除低优先级配置来测试高优先级配置是否生效 +- 使用 `printenv | grep UNILABOS` 查看所有相关环境变量 + +## 配置验证 + +### 检查配置是否生效 + +启动 Uni-Lab 时,控制台会输出配置加载信息: + +``` +[ENV] 配置文件 /path/to/config.py 加载成功 +[ENV] 设置 BasicConfig.log_level = INFO +传入了ak参数,优先采用传入参数! +传入了sk参数,优先采用传入参数! +``` + +### 常见配置错误 + +1. **配置文件格式错误** + + ``` + [ENV] 加载配置文件 /path/to/config.py 失败 + ``` + + **解决方案**:检查 Python 语法,确保配置类定义正确 + +2. **环境变量格式错误** + + ``` + [ENV] 环境变量格式不正确:UNILABOS_INVALID_VAR + ``` + + **解决方案**:确保环境变量遵循 `UNILABOS_<类名>_<字段名>` 格式 + +3. **类或字段不存在** + ``` + [ENV] 未找到类:UNKNOWNCONFIG + [ENV] 类 BasicConfig 中未找到字段:UNKNOWN_FIELD + ``` + **解决方案**:检查配置类名和字段名是否正确 + +## 相关文档 + +- [工作目录详解](working_directory.md) +- [启动参数详解](../user_guide/launch.md) +- [快速安装指南](../user_guide/quick_install_guide.md) diff --git a/docs/advanced_usage/image/copy_aksk.gif b/docs/advanced_usage/image/copy_aksk.gif new file mode 100644 index 00000000..760dd011 Binary files /dev/null and b/docs/advanced_usage/image/copy_aksk.gif differ diff --git a/docs/advanced_usage/working_directory.md b/docs/advanced_usage/working_directory.md new file mode 100644 index 00000000..37215d72 --- /dev/null +++ b/docs/advanced_usage/working_directory.md @@ -0,0 +1,218 @@ +# 工作目录详解 + +本文档详细介绍 Uni-Lab 工作目录(`working_dir`)的判断逻辑和详细用法。 + +## 什么是工作目录 + +工作目录是 Uni-Lab 存储配置文件、日志和运行数据的目录。默认情况下,工作目录为 `当前目录/unilabos_data`。 + +## 工作目录判断逻辑 + +系统按以下决策树自动确定工作目录: + +### 第一步:初始判断 + +```python +# 检查当前目录 +if 当前目录以 "unilabos_data" 结尾: + working_dir = 当前目录的绝对路径 +else: + working_dir = 当前目录/unilabos_data +``` + +**解释:** +- 如果您已经在 `unilabos_data` 目录内启动,系统直接使用当前目录 +- 否则,系统会在当前目录下创建或使用 `unilabos_data` 子目录 + +### 第二步:处理 `--working_dir` 参数 + +如果用户指定了 `--working_dir` 参数: + +```python +working_dir = 用户指定的路径 +``` + +此时还会检查配置文件: +- 如果同时指定了 `--config` 但该文件不存在 +- 系统会尝试在 `working_dir/local_config.py` 查找 +- 如果仍未找到,报错退出 + +### 第三步:处理 `--config` 参数 + +如果用户指定了 `--config` 且文件存在: + +```python +# 工作目录改为配置文件所在目录 +working_dir = config_path 的父目录 +``` + +**重要:** 这意味着配置文件的位置会影响工作目录的判断。 + +## 使用场景示例 + +### 场景 1:默认场景(推荐) + +```bash +# 当前目录:/home/user/project +unilab --ak your_ak --sk your_sk -g graph.json + +# 结果: +# working_dir = /home/user/project/unilabos_data +# config_path = /home/user/project/unilabos_data/local_config.py +``` + +### 场景 2:在 unilabos_data 目录内启动 + +```bash +cd /home/user/project/unilabos_data +unilab --ak your_ak --sk your_sk -g graph.json + +# 结果: +# working_dir = /home/user/project/unilabos_data +# config_path = /home/user/project/unilabos_data/local_config.py +``` + +### 场景 3:手动指定工作目录 + +```bash +unilab --working_dir /custom/path --ak your_ak --sk your_sk -g graph.json + +# 结果: +# working_dir = /custom/path +# config_path = /custom/path/local_config.py (如果存在) +``` + +### 场景 4:通过配置文件路径推断工作目录 + +```bash +unilab --config /data/lab_a/local_config.py --ak your_ak --sk your_sk -g graph.json + +# 结果: +# working_dir = /data/lab_a +# config_path = /data/lab_a/local_config.py +``` + +## 高级用法:管理多个实验室配置 + +### 方法 1:使用不同的工作目录 + +```bash +# 实验室 A +unilab --working_dir ~/labs/lab_a --ak ak_a --sk sk_a -g graph_a.json + +# 实验室 B +unilab --working_dir ~/labs/lab_b --ak ak_b --sk sk_b -g graph_b.json +``` + +### 方法 2:使用不同的配置文件 + +```bash +# 实验室 A +unilab --config ~/labs/lab_a/config.py --ak ak_a --sk sk_a -g graph_a.json + +# 实验室 B +unilab --config ~/labs/lab_b/config.py --ak ak_b --sk sk_b -g graph_b.json +``` + +### 方法 3:使用shell脚本管理 + +创建 `start_lab_a.sh`: + +```bash +#!/bin/bash +cd ~/labs/lab_a +unilab --ak your_ak_a --sk your_sk_a -g graph_a.json +``` + +创建 `start_lab_b.sh`: + +```bash +#!/bin/bash +cd ~/labs/lab_b +unilab --ak your_ak_b --sk your_sk_b -g graph_b.json +``` + +## 完整决策流程图 + +``` +开始 + ↓ +判断当前目录是否以 unilabos_data 结尾? + ├─ 是 → working_dir = 当前目录 + └─ 否 → working_dir = 当前目录/unilabos_data + ↓ +用户是否指定 --working_dir? + └─ 是 → working_dir = 指定路径 + ↓ +用户是否指定 --config 且文件存在? + └─ 是 → working_dir = config 文件所在目录 + ↓ +检查 working_dir/local_config.py 是否存在? + ├─ 是 → 加载配置文件 → 继续启动 + └─ 否 → 询问是否首次使用 + ├─ 是 → 创建目录和配置文件 → 继续启动 + └─ 否 → 退出程序 +``` + +## 常见问题 + +### 1. 如何查看当前使用的工作目录? + +启动 Uni-Lab 时,系统会在控制台输出: + +``` +当前工作目录为 /path/to/working_dir +``` + +### 2. 可以在同一台机器上运行多个实验室吗? + +可以。使用不同的工作目录或配置文件即可: + +```bash +# 终端 1 +unilab --working_dir ~/lab1 --ak ak1 --sk sk1 -g graph1.json + +# 终端 2 +unilab --working_dir ~/lab2 --ak ak2 --sk sk2 -g graph2.json +``` + +### 3. 工作目录中存储了什么? + +- `local_config.py` - 配置文件 +- 日志文件 +- 临时运行数据 +- 缓存文件 + +### 4. 可以删除工作目录吗? + +可以,但会丢失: +- 配置文件(需要重新创建) +- 历史日志 +- 缓存数据 + +建议定期备份配置文件。 + +### 5. 如何迁移到新的工作目录? + +```bash +# 1. 复制旧的工作目录 +cp -r ~/old_path/unilabos_data ~/new_path/unilabos_data + +# 2. 在新位置启动 +cd ~/new_path +unilab --ak your_ak --sk your_sk -g graph.json +``` + +## 最佳实践 + +1. **使用默认工作目录**:对于单一实验室,使用默认的 `./unilabos_data` 即可 +2. **组织多实验室**:为每个实验室创建独立的目录结构 +3. **版本控制**:将配置文件纳入版本控制,但排除日志和缓存 +4. **备份配置**:定期备份 `local_config.py` 文件 +5. **使用脚本**:为不同实验室创建启动脚本,简化操作 + +## 相关文档 + +- [配置文件指南](configuration.md) +- [启动参数详解](../user_guide/launch.md) + diff --git a/docs/concepts/01-communication-instruction.md b/docs/concepts/01-communication-instruction.md index 09e69754..8b0c040f 100644 --- a/docs/concepts/01-communication-instruction.md +++ b/docs/concepts/01-communication-instruction.md @@ -1,7 +1,7 @@ (instructions)= # 设备抽象、指令集与通信中间件 -Uni-Lab 操作系统的目的是将不同类型和厂家的实验仪器进行抽象统一,对应用层提供服务。因此,理清实验室设备之间的业务逻辑至关重要。 +Uni-Lab-OS的目的是将不同类型和厂家的实验仪器进行抽象统一,对应用层提供服务。因此,理清实验室设备之间的业务逻辑至关重要。 ## 设备间通信模式 diff --git a/docs/developer_guide/action_includes.md b/docs/developer_guide/action_includes.md index ee145bfb..206f94e2 100644 --- a/docs/developer_guide/action_includes.md +++ b/docs/developer_guide/action_includes.md @@ -1,5 +1,4 @@ -## 简单单变量动作函数 - +## 基础通用操作 ### `SendCmd` @@ -7,49 +6,343 @@ :language: yaml ``` ----- -## 常量有机化学操作 +--- -Uni-Lab 常量有机化学指令集多数来自 [XDL](https://croningroup.gitlab.io/chemputer/xdl/standard/full_steps_specification.html#),包含有机合成实验中常见的操作,如加热、搅拌、冷却等。 +### `FloatSingleInput` - - -### `Clean` - -```{literalinclude} ../../unilabos_msgs/action/Clean.action +```{literalinclude} ../../unilabos_msgs/action/FloatSingleInput.action :language: yaml ``` ----- +--- -### `HeatChillStart` +### `IntSingleInput` -```{literalinclude} ../../unilabos_msgs/action/HeatChillStart.action +```{literalinclude} ../../unilabos_msgs/action/IntSingleInput.action :language: yaml ``` ----- +--- -### `HeatChillStop` +### `Point3DSeparateInput` -```{literalinclude} ../../unilabos_msgs/action/HeatChillStop.action +```{literalinclude} ../../unilabos_msgs/action/Point3DSeparateInput.action :language: yaml ``` ----- +--- -### `PumpTransfer` +### `StrSingleInput` + +```{literalinclude} ../../unilabos_msgs/action/StrSingleInput.action +:language: yaml +``` + +--- + +### `Wait` + +```{literalinclude} ../../unilabos_msgs/action/Wait.action +:language: yaml +``` + +--- + +## 化学实验操作 + +Uni-Lab 化学操作指令集多数来自 [XDL](https://croningroup.gitlab.io/chemputer/xdl/standard/full_steps_specification.html#),包含有机合成实验中常见的操作。 + +### 物料添加 + +#### `Add` + +```{literalinclude} ../../unilabos_msgs/action/Add.action +:language: yaml +``` + +--- + +#### `AddSolid` + +```{literalinclude} ../../unilabos_msgs/action/AddSolid.action +:language: yaml +``` + +--- + +### 液体转移与泵控制 + +#### `PumpTransfer` ```{literalinclude} ../../unilabos_msgs/action/PumpTransfer.action :language: yaml ``` ----- -## 移液工作站及相关生物自动化设备操作 +--- -Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.org/user_guide/index.html),包含生物实验中常见的操作,如移液、混匀、离心等。 +#### `SetPumpPosition` +```{literalinclude} ../../unilabos_msgs/action/SetPumpPosition.action +:language: yaml +``` +--- + +#### `Transfer` + +```{literalinclude} ../../unilabos_msgs/action/Transfer.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 +``` + +--- + +### 搅拌控制 + +#### `StartStir` + +```{literalinclude} ../../unilabos_msgs/action/StartStir.action +:language: yaml +``` + +--- + +#### `Stir` + +```{literalinclude} ../../unilabos_msgs/action/Stir.action +:language: yaml +``` + +--- + +#### `StopStir` + +```{literalinclude} ../../unilabos_msgs/action/StopStir.action +:language: yaml +``` + +--- + +### 气体与真空控制 + +#### `EvacuateAndRefill` + +```{literalinclude} ../../unilabos_msgs/action/EvacuateAndRefill.action +:language: yaml +``` + +--- + +#### `Purge` + +```{literalinclude} ../../unilabos_msgs/action/Purge.action +:language: yaml +``` + +--- + +#### `StartPurge` + +```{literalinclude} ../../unilabos_msgs/action/StartPurge.action +:language: yaml +``` + +--- + +#### `StopPurge` + +```{literalinclude} ../../unilabos_msgs/action/StopPurge.action +:language: yaml +``` + +--- + +### 分离与过滤 + +#### `Centrifuge` + +```{literalinclude} ../../unilabos_msgs/action/Centrifuge.action +:language: yaml +``` + +--- + +#### `Filter` + +```{literalinclude} ../../unilabos_msgs/action/Filter.action +:language: yaml +``` + +--- + +#### `FilterThrough` + +```{literalinclude} ../../unilabos_msgs/action/FilterThrough.action +:language: yaml +``` + +--- + +#### `RunColumn` + +```{literalinclude} ../../unilabos_msgs/action/RunColumn.action +:language: yaml +``` + +--- + +#### `Separate` + +```{literalinclude} ../../unilabos_msgs/action/Separate.action +:language: yaml +``` + +--- + +### 化学处理 + +#### `AdjustPH` + +```{literalinclude} ../../unilabos_msgs/action/AdjustPH.action +:language: yaml +``` + +--- + +#### `Crystallize` + +```{literalinclude} ../../unilabos_msgs/action/Crystallize.action +:language: yaml +``` + +--- + +#### `Dissolve` + +```{literalinclude} ../../unilabos_msgs/action/Dissolve.action +:language: yaml +``` + +--- + +#### `Dry` + +```{literalinclude} ../../unilabos_msgs/action/Dry.action +:language: yaml +``` + +--- + +#### `Evaporate` + +```{literalinclude} ../../unilabos_msgs/action/Evaporate.action +:language: yaml +``` + +--- + +#### `Hydrogenate` + +```{literalinclude} ../../unilabos_msgs/action/Hydrogenate.action +:language: yaml +``` + +--- + +#### `Recrystallize` + +```{literalinclude} ../../unilabos_msgs/action/Recrystallize.action +:language: yaml +``` + +--- + +#### `WashSolid` + +```{literalinclude} ../../unilabos_msgs/action/WashSolid.action +:language: yaml +``` + +--- + +### 清洁与维护 + +#### `Clean` + +```{literalinclude} ../../unilabos_msgs/action/Clean.action +:language: yaml +``` + +--- + +#### `CleanVessel` + +```{literalinclude} ../../unilabos_msgs/action/CleanVessel.action +:language: yaml +``` + +--- + +#### `EmptyIn` + +```{literalinclude} ../../unilabos_msgs/action/EmptyIn.action +:language: yaml +``` + +--- + +#### `ResetHandling` + +```{literalinclude} ../../unilabos_msgs/action/ResetHandling.action +:language: yaml +``` + +--- + +## 生物自动化操作 + +Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.org/user_guide/index.html),包含移液工作站的各类操作。 + +### `LiquidHandlerAdd` + +```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerAdd.action +:language: yaml +``` + +--- + +### `LiquidHandlerAspirate` + +```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerAspirate.action +:language: yaml +``` + +--- ### `LiquidHandlerDiscardTips` @@ -57,7 +350,15 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o :language: yaml ``` ----- +--- + +### `LiquidHandlerDispense` + +```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerDispense.action +:language: yaml +``` + +--- ### `LiquidHandlerDropTips` @@ -65,7 +366,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o :language: yaml ``` ----- +--- ### `LiquidHandlerDropTips96` @@ -73,7 +374,31 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o :language: yaml ``` ----- +--- + +### `LiquidHandlerIncubateBiomek` + +```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerIncubateBiomek.action +:language: yaml +``` + +--- + +### `LiquidHandlerMix` + +```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerMix.action +:language: yaml +``` + +--- + +### `LiquidHandlerMoveBiomek` + +```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerMoveBiomek.action +:language: yaml +``` + +--- ### `LiquidHandlerMoveLid` @@ -81,7 +406,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o :language: yaml ``` ----- +--- ### `LiquidHandlerMovePlate` @@ -89,7 +414,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o :language: yaml ``` ----- +--- ### `LiquidHandlerMoveResource` @@ -97,7 +422,23 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o :language: yaml ``` ----- +--- + +### `LiquidHandlerMoveTo` + +```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerMoveTo.action +:language: yaml +``` + +--- + +### `LiquidHandlerOscillateBiomek` + +```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerOscillateBiomek.action +:language: yaml +``` + +--- ### `LiquidHandlerPickUpTips` @@ -105,7 +446,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o :language: yaml ``` ----- +--- ### `LiquidHandlerPickUpTips96` @@ -113,7 +454,23 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o :language: yaml ``` ----- +--- + +### `LiquidHandlerProtocolCreation` + +```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerProtocolCreation.action +:language: yaml +``` + +--- + +### `LiquidHandlerRemove` + +```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerRemove.action +:language: yaml +``` + +--- ### `LiquidHandlerReturnTips` @@ -121,7 +478,7 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o :language: yaml ``` ----- +--- ### `LiquidHandlerReturnTips96` @@ -129,7 +486,31 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o :language: yaml ``` ----- +--- + +### `LiquidHandlerSetGroup` + +```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerSetGroup.action +:language: yaml +``` + +--- + +### `LiquidHandlerSetLiquid` + +```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerSetLiquid.action +:language: yaml +``` + +--- + +### `LiquidHandlerSetTipRack` + +```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerSetTipRack.action +:language: yaml +``` + +--- ### `LiquidHandlerStamp` @@ -137,31 +518,215 @@ Uni-Lab 生物操作指令集多数来自 [PyLabRobot](https://docs.pylabrobot.o :language: yaml ``` ----- -## 多工作站及小车运行、物料转移 +--- +### `LiquidHandlerTransfer` -### `AGVTransfer` +```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerTransfer.action +:language: yaml +``` + +--- + +### `LiquidHandlerTransferBiomek` + +```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerTransferBiomek.action +:language: yaml +``` + +--- + +### `LiquidHandlerTransferGroup` + +```{literalinclude} ../../unilabos_msgs/action/LiquidHandlerTransferGroup.action +:language: yaml +``` + +--- + +## 专用工作站操作 + +### 反应工作站 + +#### `ReactionStationDripBack` + +```{literalinclude} ../../unilabos_msgs/action/ReactionStationDripBack.action +:language: yaml +``` + +--- + +#### `ReactionStationLiquidFeedBeaker` + +```{literalinclude} ../../unilabos_msgs/action/ReactionStationLiquidFeedBeaker.action +:language: yaml +``` + +--- + +#### `ReactionStationLiquidFeedSolvents` + +```{literalinclude} ../../unilabos_msgs/action/ReactionStationLiquidFeedSolvents.action +:language: yaml +``` + +--- + +#### `ReactionStationLiquidFeedTitration` + +```{literalinclude} ../../unilabos_msgs/action/ReactionStationLiquidFeedTitration.action +:language: yaml +``` + +--- + +#### `ReactionStationLiquidFeedVialsNonTitration` + +```{literalinclude} ../../unilabos_msgs/action/ReactionStationLiquidFeedVialsNonTitration.action +:language: yaml +``` + +--- + +#### `ReactionStationProExecu` + +```{literalinclude} ../../unilabos_msgs/action/ReactionStationProExecu.action +:language: yaml +``` + +--- + +#### `ReactionStationReactorTakenOut` + +```{literalinclude} ../../unilabos_msgs/action/ReactionStationReactorTakenOut.action +:language: yaml +``` + +--- + +#### `ReactionStationReaTackIn` + +```{literalinclude} ../../unilabos_msgs/action/ReactionStationReaTackIn.action +:language: yaml +``` + +--- + +#### `ReactionStationSolidFeedVial` + +```{literalinclude} ../../unilabos_msgs/action/ReactionStationSolidFeedVial.action +:language: yaml +``` + +--- + +### 固体分配站 + +#### `SolidDispenseAddPowderTube` + +```{literalinclude} ../../unilabos_msgs/action/SolidDispenseAddPowderTube.action +:language: yaml +``` + +--- + +### 分液工作站 + +#### `DispenStationSolnPrep` + +```{literalinclude} ../../unilabos_msgs/action/DispenStationSolnPrep.action +:language: yaml +``` + +--- + +#### `DispenStationVialFeed` + +```{literalinclude} ../../unilabos_msgs/action/DispenStationVialFeed.action +:language: yaml +``` + +--- + +### 后处理工作站 + +#### `PostProcessGrab` + +```{literalinclude} ../../unilabos_msgs/action/PostProcessGrab.action +:language: yaml +``` + +--- + +#### `PostProcessTriggerClean` + +```{literalinclude} ../../unilabos_msgs/action/PostProcessTriggerClean.action +:language: yaml +``` + +--- + +#### `PostProcessTriggerPostPro` + +```{literalinclude} ../../unilabos_msgs/action/PostProcessTriggerPostPro.action +:language: yaml +``` + +--- + +## 系统管理与资源调度 + +### 资源与布局管理 + +#### `DefaultLayoutRecommendLayout` + +```{literalinclude} ../../unilabos_msgs/action/DefaultLayoutRecommendLayout.action +:language: yaml +``` + +--- + +#### `ResourceCreateFromOuter` + +```{literalinclude} ../../unilabos_msgs/action/ResourceCreateFromOuter.action +:language: yaml +``` + +--- + +#### `ResourceCreateFromOuterEasy` + +```{literalinclude} ../../unilabos_msgs/action/ResourceCreateFromOuterEasy.action +:language: yaml +``` + +--- + +### 多工作站协调 + +#### `AGVTransfer` ```{literalinclude} ../../unilabos_msgs/action/AGVTransfer.action :language: yaml ``` ----- +--- -### `WorkStationRun` +#### `WorkStationRun` ```{literalinclude} ../../unilabos_msgs/action/WorkStationRun.action :language: yaml ``` ----- -## 机械臂、夹爪等机器人设备 +--- -Uni-Lab 机械臂、机器人、夹爪和导航指令集沿用 ROS2 的 `control_msgs` 和 `nav2_msgs`: +## 机器人控制(ROS2 标准) +Uni-Lab 机械臂、机器人、夹爪和导航指令集沿用 ROS2 的 `control_msgs` 和 `nav2_msgs`。 -### `FollowJointTrajectory` +### 机械臂与关节控制 + +#### `FollowJointTrajectory` ```yaml # The trajectory for all revolute, continuous or prismatic joints @@ -228,8 +793,55 @@ trajectory_msgs/MultiDOFJointTrajectoryPoint multi_dof_error ``` ----- -### `GripperCommand` +--- + +#### `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 +``` + +--- + +### 夹爪控制 + +#### `GripperCommand` ```yaml GripperCommand command @@ -246,18 +858,9 @@ bool reached_goal # True iff the gripper position has reached the commanded setp ``` ----- -### `JointTrajectory` - -```yaml -trajectory_msgs/JointTrajectory trajectory ---- --- -``` - ----- -### `ParallelGripperCommand` +#### `ParallelGripperCommand` ```yaml # Parallel grippers refer to an end effector where two opposing fingers grasp an object from opposite sides. @@ -281,39 +884,11 @@ sensor_msgs/JointState state # The current gripper state. ``` ----- -### `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` +#### `AssistedTeleop` ```yaml #goal definition @@ -324,11 +899,11 @@ builtin_interfaces/Duration total_elapsed_time --- #feedback builtin_interfaces/Duration current_teleop_duration - ``` ----- -### `BackUp` +--- + +#### `BackUp` ```yaml #goal definition @@ -341,11 +916,11 @@ builtin_interfaces/Duration total_elapsed_time --- #feedback definition float32 distance_traveled - ``` ----- -### `ComputePathThroughPoses` +--- + +#### `ComputePathThroughPoses` ```yaml #goal definition @@ -359,11 +934,11 @@ nav_msgs/Path path builtin_interfaces/Duration planning_time --- #feedback definition - ``` ----- -### `ComputePathToPose` +--- + +#### `ComputePathToPose` ```yaml #goal definition @@ -377,11 +952,11 @@ nav_msgs/Path path builtin_interfaces/Duration planning_time --- #feedback definition - ``` ----- -### `DriveOnHeading` +--- + +#### `DriveOnHeading` ```yaml #goal definition @@ -394,11 +969,11 @@ builtin_interfaces/Duration total_elapsed_time --- #feedback definition float32 distance_traveled - ``` ----- -### `DummyBehavior` +--- + +#### `DummyBehavior` ```yaml #goal definition @@ -408,11 +983,11 @@ std_msgs/String command builtin_interfaces/Duration total_elapsed_time --- #feedback definition - ``` ----- -### `FollowPath` +--- + +#### `FollowPath` ```yaml #goal definition @@ -426,11 +1001,11 @@ std_msgs/Empty result #feedback definition float32 distance_to_goal float32 speed - ``` ----- -### `FollowWaypoints` +--- + +#### `FollowWaypoints` ```yaml #goal definition @@ -441,11 +1016,11 @@ int32[] missed_waypoints --- #feedback definition uint32 current_waypoint - ``` ----- -### `NavigateThroughPoses` +--- + +#### `NavigateThroughPoses` ```yaml #goal definition @@ -462,11 +1037,11 @@ builtin_interfaces/Duration estimated_time_remaining int16 number_of_recoveries float32 distance_remaining int16 number_of_poses_remaining - ``` ----- -### `NavigateToPose` +--- + +#### `NavigateToPose` ```yaml #goal definition @@ -482,11 +1057,11 @@ builtin_interfaces/Duration navigation_time builtin_interfaces/Duration estimated_time_remaining int16 number_of_recoveries float32 distance_remaining - ``` ----- -### `SmoothPath` +--- + +#### `SmoothPath` ```yaml #goal definition @@ -501,11 +1076,11 @@ builtin_interfaces/Duration smoothing_duration bool was_completed --- #feedback definition - ``` ----- -### `Spin` +--- + +#### `Spin` ```yaml #goal definition @@ -517,11 +1092,13 @@ builtin_interfaces/Duration total_elapsed_time --- #feedback definition float32 angular_distance_traveled - ``` ----- -### `Wait` +--- + +#### `Wait` (Nav2) + +> **注意**:这是 ROS2 nav2_msgs 的标准 Wait action,与 unilabos_msgs 的 Wait action 不同。 ```yaml #goal definition @@ -532,7 +1109,6 @@ builtin_interfaces/Duration total_elapsed_time --- #feedback definition builtin_interfaces/Duration time_left - ``` ----- +--- diff --git a/docs/developer_guide/add_device.md b/docs/developer_guide/add_device.md index 0cec54da..dc95274f 100644 --- a/docs/developer_guide/add_device.md +++ b/docs/developer_guide/add_device.md @@ -1,10 +1,18 @@ -# 添加新设备 +# 添加设备:编写驱动 -在 Uni-Lab 中,设备(Device)是实验操作的基础单元。Uni-Lab 使用**注册表机制**来兼容管理种类繁多的设备驱动程序。回顾 {ref}`instructions` 中的概念,抽象的设备对外拥有【话题】【服务】【动作】三种通信机制,因此将设备添加进 Uni-Lab,实际上是将设备驱动中的三种机制映射到 Uni-Lab 标准指令集上。 +在 Uni-Lab 中,设备(Device)是实验操作的基础单元。Uni-Lab 使用**注册表机制**来兼容管理种类繁多的设备驱动程序。抽象的设备对外拥有【话题】【服务】【动作】三种通信机制,因此将设备添加进 Uni-Lab,实际上是将设备驱动中的这三种机制映射到 Uni-Lab 标准指令集上。 -能被 Uni-Lab 添加的驱动程序类型有以下种类: +> **💡 提示:** 本文档介绍如何使用已有的设备驱动(SDK)。若设备没有现成的驱动程序,需要自己开发驱动,请参考 {doc}`add_old_device`。 -1. Python Class,如 +## 支持的驱动类型 + +Uni-Lab 支持以下两种驱动程序: + +### 1. Python Class(推荐) + +Python 类设备驱动在完成注册表后可以直接在 Uni-Lab 中使用,无需额外编译。 + +**示例:** ```python class MockGripper: @@ -31,12 +39,11 @@ class MockGripper: def status(self) -> str: return self._status - # 会被自动识别的设备动作,接入 Uni-Lab 时会作为 ActionServer 接受任意控制者的指令 @status.setter def status(self, target): self._status = target - # 需要在注册表添加的设备动作,接入 Uni-Lab 时会作为 ActionServer 接受任意控制者的指令 + # 会被自动识别的设备动作,接入 Uni-Lab 时会作为 ActionServer 接受任意控制者的指令 def push_to(self, position: float, torque: float, velocity: float = 0.0): self._status = "Running" current_pos = self.position @@ -53,9 +60,11 @@ class MockGripper: self._status = "Idle" ``` -Python 类设备驱动在完成注册表后可以直接在 Uni-Lab 使用。 +### 2. C# Class -2. C# Class,如 +C# 驱动设备在完成注册表后,需要调用 Uni-Lab C# 编译后才能使用(仅需一次)。 + +**示例:** ```csharp using System; @@ -84,7 +93,7 @@ public class MockGripper position = currentPos + (Position - currentPos) / 20 * (i + 1); torque = Torque / (20 - i); velocity = Velocity; - await Task.Delay((int)(moveTime * 1000 / 20)); // Convert seconds to milliseconds + await Task.Delay((int)(moveTime * 1000 / 20)); } torque = Torque; status = "Idle"; @@ -92,12 +101,16 @@ public class MockGripper } ``` -C# 驱动设备在完成注册表后,需要调用 Uni-Lab C# 编译后才能使用,但只需一次。 +--- -## 快速开始:使用注册表编辑器(推荐) +## 快速开始:两种方式添加设备 + +### 方式 1:使用注册表编辑器(推荐) 推荐使用 Uni-Lab-OS 自带的可视化编辑器,它能自动分析您的设备驱动并生成大部分配置: +**步骤:** + 1. 启动 Uni-Lab-OS 2. 在浏览器中打开"注册表编辑器"页面 3. 选择您的 Python 设备驱动文件 @@ -106,13 +119,18 @@ C# 驱动设备在完成注册表后,需要调用 Uni-Lab C# 编译后才能 6. 点击"生成注册表",复制生成的内容 7. 保存到 `devices/` 目录下 ---- +**优点:** -## 手动编写注册表(简化版) +- 自动识别设备属性和方法 +- 可视化界面,易于操作 +- 自动生成完整配置 +- 减少手动配置错误 + +### 方式 2:手动编写注册表(简化版) 如果需要手动编写,只需要提供两个必需字段,系统会自动补全其余内容: -### 最小配置示例 +**最小配置示例:** ```yaml my_device: # 设备唯一标识符 @@ -121,22 +139,22 @@ my_device: # 设备唯一标识符 type: python # 驱动类型 ``` -### 注册表文件位置 +**注册表文件位置:** - 默认路径:`unilabos/registry/devices` -- 自定义路径:启动时使用 `--registry` 参数指定 -- 可将多个设备写在同一个 yaml 文件中 +- 自定义路径:启动时使用 `--registry_path` 参数指定 +- 可将多个设备写在同一个 YAML 文件中 -### 系统自动生成的内容 +**系统自动生成的内容:** 系统会自动分析您的 Python 驱动类并生成: -- `status_types`:从 `get_*` 方法自动识别状态属性 +- `status_types`:从 `@property` 装饰的方法自动识别状态属性 - `action_value_mappings`:从类方法自动生成动作映射 - `init_param_schema`:从 `__init__` 方法分析初始化参数 - `schema`:前端显示用的属性类型定义 -### 完整结构概览 +**完整结构概览:** ```yaml my_device: @@ -151,4 +169,848 @@ my_device: schema: {} # 自动生成 ``` -详细的注册表编写指南和高级配置,请参考{doc}`yaml 注册表编写指南 `。 +> 💡 **提示:** 详细的注册表编写指南和高级配置,请参考 {doc}`03_add_device_registry`。 + +--- + +## Python 类结构要求 + +Uni-Lab 设备驱动是一个 Python 类,需要遵循以下结构: + +```python +from typing import Dict, Any + +class MyDevice: + """设备类文档字符串 + + 说明设备的功能、连接方式等 + """ + + def __init__(self, config: Dict[str, Any]): + """初始化设备 + + Args: + config: 配置字典,来自图文件或注册表 + """ + self.port = config.get('port', '/dev/ttyUSB0') + self.baudrate = config.get('baudrate', 9600) + self._status = "idle" + # 初始化硬件连接 + + @property + def status(self) -> str: + """设备状态(会自动广播)""" + return self._status + + def my_action(self, param: float) -> Dict[str, Any]: + """执行动作 + + Args: + param: 参数说明 + + Returns: + {"success": True, "result": ...} + """ + # 执行设备操作 + return {"success": True} +``` + +## 状态属性 vs 动作方法 + +### 状态属性(@property) + +状态属性会被自动识别并定期广播: + +```python +@property +def temperature(self) -> float: + """当前温度""" + return self._read_temperature() + +@property +def status(self) -> str: + """设备状态: idle, running, error""" + return self._status + +@property +def is_ready(self) -> bool: + """设备是否就绪""" + return self._status == "idle" +``` + +**特点**: + +- 使用`@property`装饰器 +- 只读,不能有参数 +- 自动添加到注册表的`status_types` +- 定期发布到 ROS2 topic + +### 动作方法 + +动作方法是设备可以执行的操作: + +```python +def start_heating(self, target_temp: float, rate: float = 1.0) -> Dict[str, Any]: + """开始加热 + + Args: + target_temp: 目标温度(°C) + rate: 升温速率(°C/min) + + Returns: + {"success": bool, "message": str} + """ + self._status = "heating" + self._target_temp = target_temp + # 发送命令到硬件 + return {"success": True, "message": f"Heating to {target_temp}°C"} + +async def async_operation(self, duration: float) -> Dict[str, Any]: + """异步操作(长时间运行) + + Args: + duration: 持续时间(秒) + """ + # 使用 self.sleep 而不是 asyncio.sleep(ROS2 异步机制) + await self.sleep(duration) + return {"success": True} +``` + +**特点**: + +- 普通方法或 async 方法 +- 返回 Dict 类型的结果 +- 自动注册为 ROS2 Action +- 支持参数和返回值 + +### 返回值设计指南 + +> **⚠️ 重要:返回值会自动显示在前端** +> +> 动作方法的返回值(字典)会自动显示在 Web 界面的工作流执行结果中。因此,**强烈建议**设计结构化、可读的返回值字典。 + +**推荐的返回值结构:** + +```python +def my_action(self, param: float) -> Dict[str, Any]: + """执行操作""" + try: + # 执行操作... + result = self._do_something(param) + + return { + "success": True, # 必需:操作是否成功 + "message": "操作完成", # 推荐:用户友好的消息 + "result": result, # 可选:具体结果数据 + "param_used": param, # 可选:记录使用的参数 + # 其他有用的信息... + } + except Exception as e: + return { + "success": False, + "error": str(e), + "message": "操作失败" + } +``` + +**最佳实践示例(参考 `host_node.test_latency`):** + +```python +def test_latency(self) -> Dict[str, Any]: + """测试网络延迟 + + 返回值会在前端显示,包含详细的测试结果 + """ + # 执行测试... + avg_rtt_ms = 25.5 + avg_time_diff_ms = 10.2 + test_count = 5 + + # 返回结构化的测试结果 + return { + "status": "success", # 状态标识 + "avg_rtt_ms": avg_rtt_ms, # 平均往返时间 + "avg_time_diff_ms": avg_time_diff_ms, # 平均时间差 + "max_time_error_ms": 5.3, # 最大误差 + "task_delay_ms": 15.7, # 任务延迟 + "test_count": test_count, # 测试次数 + } +``` + +**前端显示效果:** + +当用户在 Web 界面执行工作流时,返回的字典会以 JSON 格式显示在结果面板中: + +```json +{ + "status": "success", + "avg_rtt_ms": 25.5, + "avg_time_diff_ms": 10.2, + "max_time_error_ms": 5.3, + "task_delay_ms": 15.7, + "test_count": 5 +} +``` + +**返回值设计建议:** + +1. **始终包含 `success` 字段**:布尔值,表示操作是否成功 +2. **包含 `message` 字段**:字符串,提供用户友好的描述 +3. **使用有意义的键名**:使用描述性的键名(如 `avg_rtt_ms` 而不是 `v1`) +4. **包含单位**:在键名中包含单位(如 `_ms`、`_ml`、`_celsius`) +5. **记录重要参数**:返回使用的关键参数值,便于追溯 +6. **错误信息详细**:失败时包含 `error` 字段和详细的错误描述 +7. **避免返回大数据**:不要返回大型数组或二进制数据,这会影响前端性能 + +**错误处理示例:** + +```python +def risky_operation(self, param: float) -> Dict[str, Any]: + """可能失败的操作""" + if param < 0: + return { + "success": False, + "error": "参数不能为负数", + "message": f"无效参数: {param}", + "param": param + } + + try: + result = self._execute(param) + return { + "success": True, + "message": "操作成功", + "result": result, + "param": param + } + except IOError as e: + return { + "success": False, + "error": "通信错误", + "message": str(e), + "device_status": self._status + } +``` + +## 特殊参数类型:ResourceSlot 和 DeviceSlot + +Uni-Lab 提供特殊的参数类型,用于在方法中声明需要选择资源或设备。 + +### 导入类型 + +```python +from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot +from typing import List +``` + +### ResourceSlot - 资源选择 + +用于需要选择物料资源的场景: + +```python +def pipette_liquid( + self, + source: ResourceSlot, # 单个源容器 + target: ResourceSlot, # 单个目标容器 + volume: float +) -> Dict[str, Any]: + """从源容器吸取液体到目标容器 + + Args: + source: 源容器(前端会显示资源选择下拉框) + target: 目标容器(前端会显示资源选择下拉框) + volume: 体积(μL) + """ + print(f"Pipetting {volume}μL from {source.id} to {target.id}") + return {"success": True} +``` + +**多选示例**: + +```python +def mix_multiple( + self, + containers: List[ResourceSlot], # 多个容器选择 + speed: float +) -> Dict[str, Any]: + """混合多个容器 + + Args: + containers: 容器列表(前端会显示多选下拉框) + speed: 混合速度 + """ + for container in containers: + print(f"Mixing {container.name}") + return {"success": True} +``` + +### DeviceSlot - 设备选择 + +用于需要选择其他设备的场景: + +```python +def coordinate_with_device( + self, + other_device: DeviceSlot, # 单个设备选择 + command: str +) -> Dict[str, Any]: + """与另一个设备协同工作 + + Args: + other_device: 协同设备(前端会显示设备选择下拉框) + command: 命令 + """ + print(f"Coordinating with {other_device.name}") + return {"success": True} +``` + +**多设备示例**: + +```python +def sync_devices( + self, + devices: List[DeviceSlot], # 多个设备选择 + sync_signal: str +) -> Dict[str, Any]: + """同步多个设备 + + Args: + devices: 设备列表(前端会显示多选下拉框) + sync_signal: 同步信号 + """ + for dev in devices: + print(f"Syncing {dev.name}") + return {"success": True} +``` + +### 完整示例:液体处理工作站 + +```python +from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot +from typing import List, Dict, Any + +class LiquidHandler: + """液体处理工作站""" + + def __init__(self, config: Dict[str, Any]): + self.simulation = config.get('simulation', False) + self._status = "idle" + + @property + def status(self) -> str: + return self._status + + def transfer_liquid( + self, + source: ResourceSlot, # 源容器选择 + target: ResourceSlot, # 目标容器选择 + volume: float, + tip: ResourceSlot = None # 可选的枪头选择 + ) -> Dict[str, Any]: + """转移液体 + + 前端效果: + - source: 下拉框,列出所有可用容器 + - target: 下拉框,列出所有可用容器 + - volume: 数字输入框 + - tip: 下拉框(可选),列出所有枪头 + """ + self._status = "transferring" + + # source和target会被解析为实际的资源对象 + print(f"Transferring {volume}μL") + print(f" From: {source.id} ({source.name})") + print(f" To: {target.id} ({target.name})") + + if tip: + print(f" Using tip: {tip.id}") + + # 执行实际的液体转移 + # ... + + self._status = "idle" + return { + "success": True, + "volume_transferred": volume, + "source_id": source.id, + "target_id": target.id + } + + def multi_dispense( + self, + source: ResourceSlot, # 单个源 + targets: List[ResourceSlot], # 多个目标 + volumes: List[float] + ) -> Dict[str, Any]: + """从一个源分配到多个目标 + + 前端效果: + - source: 单选下拉框 + - targets: 多选下拉框(可选择多个容器) + - volumes: 数组输入(每个目标对应一个体积) + """ + results = [] + for target, vol in zip(targets, volumes): + print(f"Dispensing {vol}μL to {target.name}") + results.append({ + "target": target.id, + "volume": vol + }) + + return { + "success": True, + "dispense_results": results + } + + def test_with_balance( + self, + target: ResourceSlot, # 容器 + balance: DeviceSlot # 天平设备 + ) -> Dict[str, Any]: + """使用天平测量容器 + + 前端效果: + - target: 容器选择下拉框 + - balance: 设备选择下拉框(仅显示天平类型) + """ + print(f"Weighing {target.name} on {balance.name}") + + # 可以调用balance的方法 + # weight = balance.get_weight() + + return { + "success": True, + "container": target.id, + "balance_used": balance.id + } +``` + +### 工作原理 + +#### 1. 类型识别 + +注册表扫描方法签名时: + +```python +def my_method(self, resource: ResourceSlot, device: DeviceSlot): + pass +``` + +系统识别到`ResourceSlot`和`DeviceSlot`类型。 + +#### 2. 自动添加 placeholder_keys + +在注册表中自动生成: + +```yaml +my_device: + class: + action_value_mappings: + my_method: + goal: + resource: resource + device: device + placeholder_keys: + resource: unilabos_resources # 自动添加! + device: unilabos_devices # 自动添加! +``` + +#### 3. 前端 UI 生成 + +- `unilabos_resources`: 渲染为资源选择下拉框 +- `unilabos_devices`: 渲染为设备选择下拉框 + +#### 4. 运行时解析 + +用户选择资源/设备后,实际调用时会传入完整的资源/设备对象: + +```python +# 用户在前端选择了 plate_1 +# 运行时,source参数会收到完整的Resource对象 +source.id # "plate_1" +source.name # "96孔板" +source.type # "resource" +source.class_ # "corning_96_wellplate_360ul_flat" +``` + +## 支持的通信方式 + +### 1. 串口(Serial) + +```python +import serial + +class SerialDevice: + def __init__(self, config: Dict[str, Any]): + self.port = config['port'] + self.baudrate = config.get('baudrate', 9600) + self.ser = serial.Serial( + port=self.port, + baudrate=self.baudrate, + timeout=1 + ) + + def send_command(self, cmd: str) -> str: + """发送命令并读取响应""" + self.ser.write(f"{cmd}\r\n".encode()) + response = self.ser.readline().decode().strip() + return response + + def __del__(self): + if hasattr(self, 'ser') and self.ser.is_open: + self.ser.close() +``` + +### 2. TCP/IP Socket + +```python +import socket + +class TCPDevice: + def __init__(self, config: Dict[str, Any]): + self.host = config['host'] + self.port = config['port'] + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.connect((self.host, self.port)) + + def send_command(self, cmd: str) -> str: + self.sock.sendall(cmd.encode()) + response = self.sock.recv(1024).decode() + return response +``` + +### 3. Modbus + +```python +from pymodbus.client import ModbusTcpClient + +class ModbusDevice: + def __init__(self, config: Dict[str, Any]): + self.host = config['host'] + self.port = config.get('port', 502) + self.client = ModbusTcpClient(self.host, port=self.port) + self.client.connect() + + def read_register(self, address: int) -> int: + result = self.client.read_holding_registers(address, 1) + return result.registers[0] + + def write_register(self, address: int, value: int): + self.client.write_register(address, value) +``` + +### 4. OPC UA + +```python +from opcua import Client + +class OPCUADevice: + def __init__(self, config: Dict[str, Any]): + self.url = config['url'] + self.client = Client(self.url) + self.client.connect() + + def read_node(self, node_id: str): + node = self.client.get_node(node_id) + return node.get_value() + + def write_node(self, node_id: str, value): + node = self.client.get_node(node_id) + node.set_value(value) +``` + +### 5. HTTP/RPC + +```python +import requests + +class HTTPDevice: + def __init__(self, config: Dict[str, Any]): + self.base_url = config['url'] + self.auth_token = config.get('token') + + def send_command(self, endpoint: str, data: Dict) -> Dict: + url = f"{self.base_url}/{endpoint}" + headers = {'Authorization': f'Bearer {self.auth_token}'} + response = requests.post(url, json=data, headers=headers) + return response.json() +``` + +## 异步 vs 同步方法 + +### 同步方法(适合快速操作) + +```python +def quick_operation(self, param: float) -> Dict[str, Any]: + """快速操作,立即返回""" + result = self._do_something(param) + return {"success": True, "result": result} +``` + +### 异步方法(适合耗时操作) + +```python +async def long_operation(self, duration: float) -> Dict[str, Any]: + """长时间运行的操作""" + self._status = "running" + + # 使用 ROS2 提供的 sleep 方法(而不是 asyncio.sleep) + await self.sleep(duration) + + # 可以在过程中发送feedback + # 需要配合ROS2 Action的feedback机制 + + self._status = "idle" + return {"success": True, "duration": duration} +``` + +> **⚠️ 重要提示:ROS2 异步机制 vs Python asyncio** +> +> Uni-Lab 的设备驱动虽然使用 `async def` 语法,但**底层是 ROS2 的异步机制,而不是 Python 的 asyncio**。 +> +> **不能使用的 asyncio 功能:** +> +> - ❌ `asyncio.sleep()` - 会导致 ROS2 事件循环阻塞 +> - ❌ `asyncio.create_task()` - 任务不会被 ROS2 正确调度 +> - ❌ `asyncio.gather()` - 无法与 ROS2 集成 +> - ❌ 其他 asyncio 标准库函数 +> +> **应该使用的方法(继承自 BaseROS2DeviceNode):** +> +> - ✅ `await self.sleep(seconds)` - ROS2 兼容的睡眠 +> - ✅ `await self.create_task(func, **kwargs)` - ROS2 兼容的任务创建 +> - ✅ ROS2 的 Action/Service 回调机制 +> +> **示例:** +> +> ```python +> async def complex_operation(self, duration: float) -> Dict[str, Any]: +> """正确使用 ROS2 异步方法""" +> self._status = "processing" +> +> # ✅ 正确:使用 self.sleep +> await self.sleep(duration) +> +> # ✅ 正确:创建并发任务 +> task = await self.create_task(self._background_work) +> +> # ❌ 错误:不要使用 asyncio +> # await asyncio.sleep(duration) # 这会导致问题! +> # task = asyncio.create_task(...) # 这也不行! +> +> self._status = "idle" +> return {"success": True} +> +> async def _background_work(self): +> """后台任务""" +> await self.sleep(1.0) +> self.lab_logger().info("Background work completed") +> ``` +> +> **为什么不能混用?** +> +> ROS2 使用 `rclpy` 的事件循环来管理所有异步操作。如果使用 `asyncio` 的函数,这些操作会在不同的事件循环中运行,导致: +> +> - ROS2 回调无法正确执行 +> - 任务可能永远不会完成 +> - 程序可能死锁或崩溃 +> +> **参考实现:** +> +> `BaseROS2DeviceNode` 提供的方法定义(`base_device_node.py:563-572`): +> +> ```python +> async def sleep(self, rel_time: float, callback_group=None): +> """ROS2 兼容的异步睡眠""" +> if callback_group is None: +> callback_group = self.callback_group +> await ROS2DeviceNode.async_wait_for(self, rel_time, callback_group) +> +> @classmethod +> async def create_task(cls, func, trace_error=True, **kwargs) -> Task: +> """ROS2 兼容的任务创建""" +> return ROS2DeviceNode.run_async_func(func, trace_error, **kwargs) +> ``` + +## 错误处理 + +### 基本错误处理 + +```python +def operation_with_error_handling(self, param: float) -> Dict[str, Any]: + """带错误处理的操作""" + try: + result = self._risky_operation(param) + return { + "success": True, + "result": result + } + except ValueError as e: + return { + "success": False, + "error": "Invalid parameter", + "message": str(e) + } + except IOError as e: + self._status = "error" + return { + "success": False, + "error": "Communication error", + "message": str(e) + } +``` + +### 自定义异常 + +```python +class DeviceError(Exception): + """设备错误基类""" + pass + +class DeviceNotReadyError(DeviceError): + """设备未就绪""" + pass + +class DeviceTimeoutError(DeviceError): + """设备超时""" + pass + +class MyDevice: + def operation(self) -> Dict[str, Any]: + if self._status != "idle": + raise DeviceNotReadyError(f"Device is {self._status}") + + # 执行操作 + return {"success": True} +``` + +## 最佳实践 + +### 1. 类型注解 + +```python +from typing import Dict, Any, Optional, List + +def method( + self, + param1: float, + param2: str, + optional_param: Optional[int] = None +) -> Dict[str, Any]: + """完整的类型注解有助于自动生成注册表""" + pass +``` + +### 2. 文档字符串 + +```python +def method(self, param: float) -> Dict[str, Any]: + """方法简短描述 + + 更详细的说明... + + Args: + param: 参数说明,包括单位和范围 + + Returns: + Dict包含: + - success (bool): 是否成功 + - result (Any): 结果数据 + + Raises: + DeviceError: 错误情况说明 + """ + pass +``` + +### 3. 配置验证 + +```python +def __init__(self, config: Dict[str, Any]): + # 验证必需参数 + required = ['port', 'baudrate'] + for key in required: + if key not in config: + raise ValueError(f"Missing required config: {key}") + + self.port = config['port'] + self.baudrate = config['baudrate'] +``` + +### 4. 资源清理 + +```python +def __del__(self): + """析构函数,清理资源""" + if hasattr(self, 'connection') and self.connection: + self.connection.close() +``` + +### 5. 设计前端友好的返回值 + +**记住:返回值会直接显示在 Web 界面** + +```python +import time + +def measure_temperature(self) -> Dict[str, Any]: + """测量温度 + + ✅ 好的返回值设计: + - 包含 success 状态 + - 使用描述性键名 + - 在键名中包含单位 + - 记录测量时间 + """ + temp = self._read_temperature() + + return { + "success": True, + "temperature_celsius": temp, # 键名包含单位 + "timestamp": time.time(), # 记录时间 + "sensor_status": "normal", # 额外状态信息 + "message": f"温度测量完成: {temp}°C" # 用户友好的消息 + } + +def bad_example(self) -> Dict[str, Any]: + """❌ 不好的返回值设计""" + return { + "s": True, # ❌ 键名不明确 + "v": 25.5, # ❌ 没有说明单位 + "t": 1234567890, # ❌ 不清楚是什么时间戳 + } +``` + +**参考 `host_node.test_latency` 方法**(第 1216-1340 行),它返回详细的测试结果,在前端清晰显示: + +```python +return { + "status": "success", + "avg_rtt_ms": 25.5, # 有意义的键名 + 单位 + "avg_time_diff_ms": 10.2, + "max_time_error_ms": 5.3, + "task_delay_ms": 15.7, + "test_count": 5, # 记录重要信息 +} +``` + +## 下一步 + +看完本文档后,建议继续阅读: + +- {doc}`add_action` - 了解如何添加新的动作指令 +- {doc}`add_yaml` - 学习如何编写和完善 YAML 注册表 + +进阶主题: + +- {doc}`03_add_device_registry` - 了解如何配置注册表 +- {doc}`04_add_device_testing` - 学习如何测试设备 +- {doc}`add_old_device` - 没有 SDK 时如何开发设备驱动 + +## 参考 + +- [Python 类型注解](https://docs.python.org/3/library/typing.html) +- [ROS2 rclpy 异步编程](https://docs.ros.org/en/humble/Tutorials/Intermediate/Writing-an-Action-Server-Client/Py.html) - Uni-Lab 使用 ROS2 的异步机制 +- [串口通信](https://pyserial.readthedocs.io/) + +> **注意:** 虽然设备驱动使用 `async def` 语法,但请**不要参考** Python 标准的 [asyncio 文档](https://docs.python.org/3/library/asyncio.html)。Uni-Lab 使用的是 ROS2 的异步机制,两者不兼容。请使用 `self.sleep()` 和 `self.create_task()` 等 BaseROS2DeviceNode 提供的方法。 diff --git a/docs/developer_guide/device_driver.md b/docs/developer_guide/add_old_device.md similarity index 78% rename from docs/developer_guide/device_driver.md rename to docs/developer_guide/add_old_device.md index 753133a3..583d0bb2 100644 --- a/docs/developer_guide/device_driver.md +++ b/docs/developer_guide/add_old_device.md @@ -1,8 +1,10 @@ -# 设备 Driver 开发 +# 设备 Driver 开发(无 SDK 设备) 我们对设备 Driver 的定义,是一个 Python/C++/C# 类,类的方法可以用于获取传感器数据、执行设备动作、更新物料信息。它们经过 Uni-Lab 的通信中间件包装,就能成为高效分布式通信的设备节点。 -因此,若已有设备的 SDK (Driver),可以直接 [添加进 Uni-Lab](add_device.md)。仅当没有 SDK (Driver) 时,请参考本章作开发。 +因此,若已有设备的 SDK (Driver),可以直接 [添加进 Uni-Lab](add_device.md)。**仅当没有 SDK (Driver) 时,请参考本章进行驱动开发。** + +> **💡 提示:** 本文档介绍如何为没有现成驱动的老设备开发驱动程序。如果您的设备已经有 SDK 或驱动,请直接参考 {doc}`add_device`。 ## 有串口字符串指令集文档的设备:Python 串口通信(常见 RS485, RS232, USB) @@ -12,13 +14,13 @@ Modbus 与 RS485、RS232 不一样的地方在于,会有更多直接寄存器的读写,以及涉及字节序转换(Big Endian, Little Endian)。 -Uni-Lab 开发团队在仓库中提供了3个样例: +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` +- 单一机械设备**电夹爪**,通讯协议可见 [增广夹爪通讯协议](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... @@ -26,32 +28,32 @@ Uni-Lab 开发团队在仓库中提供了3个样例: ## 没有接口的老设备老软件:使用 PyWinAuto -**pywinauto**是一个 Python 库,用于自动化Windows GUI操作。它可以模拟用户的鼠标点击、键盘输入、窗口操作等,广泛应用于自动化测试、GUI自动化等场景。它支持通过两个后端进行操作: +**pywinauto**是一个 Python 库,用于自动化 Windows GUI 操作。它可以模拟用户的鼠标点击、键盘输入、窗口操作等,广泛应用于自动化测试、GUI 自动化等场景。它支持通过两个后端进行操作: -* **win32**后端:适用于大多数Windows应用程序,使用native Win32 API。(pywinauto_recorder默认使用win32后端) -* **uia**后端:基于Microsoft UI Automation,适用于较新的应用程序,特别是基于WPF或UWP的应用程序。(在win10上,会有更全的目录,有的窗口win32会识别不到) +- **win32**后端:适用于大多数 Windows 应用程序,使用 native Win32 API。(pywinauto_recorder 默认使用 win32 后端) +- **uia**后端:基于 Microsoft UI Automation,适用于较新的应用程序,特别是基于 WPF 或 UWP 的应用程序。(在 win10 上,会有更全的目录,有的窗口 win32 会识别不到) -### windows平台安装pywinauto和pywinauto_recorder +### windows 平台安装 pywinauto 和 pywinauto_recorder 直接安装会造成环境崩溃,需要下载并解压已经修改好的文件。 -cd到对应目录,执行安装 +cd 到对应目录,执行安装 -`pip install . -i ``https://pypi.tuna.tsinghua.edu.cn/simple` +` 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 平台测试 python pywinauto_recorder.py,退出使用两次 ctrl+alt+r 取消选中,关闭命令提示符。 ### 计算器例子 -你可以先打开windows的计算器,然后在ilab的环境中运行下面的代码片段,可观察到得到结果,通过这一案例,你需要掌握的pywinauto用法: +你可以先打开 windows 的计算器,然后在 ilab 的环境中运行下面的代码片段,可观察到得到结果,通过这一案例,你需要掌握的 pywinauto 用法: -* 连接到指定进程 -* 利用dump_tree查找需要的窗口 -* 获取某个位置的信息 -* 模拟点击 -* 模拟输入 +- 连接到指定进程 +- 利用 dump_tree 查找需要的窗口 +- 获取某个位置的信息 +- 模拟点击 +- 模拟输入 #### 代码学习 @@ -74,39 +76,39 @@ 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'] """ @@ -122,58 +124,58 @@ 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") @@ -187,47 +189,47 @@ 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") @@ -262,13 +264,13 @@ r, g, b = pyautogui.pixel(point_x, point_y) ### pywinauto_recorder -pywinauto_recorder是一个配合 pywinauto 使用的工具,用于录制用户的操作,并生成相应的 pywinauto 脚本。这对于一些暂时无法直接调用DLL的函数并且需要模拟用户操作的场景非常有用。同时,可以省去仅用pywinauto的一些查找UI步骤。 +pywinauto_recorder 是一个配合 pywinauto 使用的工具,用于录制用户的操作,并生成相应的 pywinauto 脚本。这对于一些暂时无法直接调用 DLL 的函数并且需要模拟用户操作的场景非常有用。同时,可以省去仅用 pywinauto 的一些查找 UI 步骤。 #### 运行尝试 -请参照 上手尝试-环境创建-3 开启pywinauto_recorder +请参照 上手尝试-环境创建-3 开启 pywinauto_recorder -例如我们这里先启动一个windows自带的计算器软件 +例如我们这里先启动一个 windows 自带的计算器软件 ![calculator_01](image/device_driver/calculator_01.png) @@ -286,7 +288,7 @@ with UIPath(u"计算器||Window"): click(u"九||Button") ``` -执行该python脚本,可以观察到新开启的计算器被点击了数字9 +执行该 python 脚本,可以观察到新开启的计算器被点击了数字 9 ![calculator_03](image/device_driver/calculator_03.png) @@ -308,23 +310,38 @@ window.dump_tree(depth=[int类型数字], filename=None) 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进行解读 +这里以上面计算器的例子对 dump_tree 进行解读 -2~4行为当前对象的窗口 +2~4 行为当前对象的窗口 -* 第2行分别是窗体的类型 `GroupBox`,窗体的题目 `数字键盘`,窗体的矩形区域坐标,对应的是屏幕上的位置(左、上、右、下) -* 第3行是 `['GroupBox', '数字键盘', '数字键盘GroupBox']`,为控件的标识符列表,可以选择任意一个,使用 `child_window(best_match="标识符")`来获取该窗口 -* 第4行是获取该控件的方法,请注意该方法不能保证获取唯一,`title`如果是变化的,也需要删除 `title`参数 +- 第 2 行分别是窗体的类型 `GroupBox`,窗体的题目 `数字键盘`,窗体的矩形区域坐标,对应的是屏幕上的位置(左、上、右、下) +- 第 3 行是 `['GroupBox', '数字键盘', '数字键盘GroupBox']`,为控件的标识符列表,可以选择任意一个,使用 `child_window(best_match="标识符")`来获取该窗口 +- 第 4 行是获取该控件的方法,请注意该方法不能保证获取唯一,`title`如果是变化的,也需要删除 `title`参数 -6~8行为当前对象窗口所包含的子窗口信息,信息类型对应2~4行 +6~8 行为当前对象窗口所包含的子窗口信息,信息类型对应 2~4 行 ### 窗口获取注意事项 -1. 在 `child_window`的时候,并不会立刻报错,只有在执行窗口的信息获取时才会调用,查询窗口是否存在,因此要想确定 `child_window`是否正确,可以调用子窗口对象的属性 `element_info`,来保证窗口存在 \ No newline at end of file +1. 在 `child_window`的时候,并不会立刻报错,只有在执行窗口的信息获取时才会调用,查询窗口是否存在,因此要想确定 `child_window`是否正确,可以调用子窗口对象的属性 `element_info`,来保证窗口存在 + +--- + +## 下一步 + +完成设备驱动开发后,建议继续阅读: + +- {doc}`add_device` - 了解如何将驱动添加到 Uni-Lab 中 +- {doc}`add_action` - 学习如何添加新的动作指令 +- {doc}`add_yaml` - 编写和完善 YAML 注册表 + +进阶主题: + +- {doc}`03_add_device_registry` - 详细的注册表配置 +- {doc}`04_add_device_testing` - 设备测试指南 diff --git a/docs/developer_guide/add_registry.md b/docs/developer_guide/add_registry.md new file mode 100644 index 00000000..122ffdd5 --- /dev/null +++ b/docs/developer_guide/add_registry.md @@ -0,0 +1,1118 @@ +# 添加设备:注册表配置完整指南 + +本文档说明如何为设备创建和配置注册表,包括基本结构、特殊类型识别、动作配置等内容。 + +## 概述 + +注册表(Registry)是Uni-Lab的设备配置系统,采用YAML格式定义设备的: +- 可用动作(Actions) +- 状态类型(Status Types) +- 初始化参数(Init Parameters) +- 连接点(Handles) + +好消息是系统会自动生成大部分配置内容,你只需要提供核心信息,让系统帮你完成剩余工作。 + +## 快速开始:使用注册表编辑器 + +推荐使用 UniLabOS 自带的可视化编辑器,它能帮你自动生成大部分配置,省去手写的麻烦。 + +### 使用步骤 + +1. 启动 UniLabOS +2. 在浏览器中打开"注册表编辑器"页面 +3. 上传你的 Python 设备驱动文件 +4. 点击"分析文件",让系统读取类信息 +5. 填写基本信息(设备描述、图标等) +6. 点击"生成注册表",复制生成的内容 +7. 保存到 `unilabos/registry/devices/your_device.yaml` + +**提示**:我们提供了测试驱动用于在界面上尝试注册表生成,参见:`test/registry/example_devices.py` + +## 注册表的基本结构 + +### 核心字段说明 + +| 字段名 | 类型 | 需要手写 | 说明 | +| ----------------- | ------ | -------- | ----------------------------------- | +| 设备标识符 | string | 是 | 设备的唯一名字,如 `mock_chiller` | +| class | object | 部分 | 设备的核心信息,必须配置 | +| description | string | 否 | 设备描述,系统默认给空字符串 | +| handles | array | 否 | 连接关系,默认为空 | +| icon | string | 否 | 图标路径,默认为空 | +| init_param_schema | object | 否 | 初始化参数,系统自动分析生成 | +| version | string | 否 | 版本号,默认 "1.0.0" | +| category | array | 否 | 设备分类,默认使用文件名 | +| config_info | array | 否 | 嵌套配置,默认为空 | +| file_path | string | 否 | 文件路径,系统自动设置 | +| registry_type | string | 否 | 注册表类型,自动设为 "device" | + +### class 字段详解 + +class 是核心部分,包含这些内容: + +| 字段名 | 类型 | 需要手写 | 说明 | +| --------------------- | ------ | -------- | ---------------------------------- | +| module | string | 是 | Python 类的路径,必须写 | +| type | string | 是 | 驱动类型,一般写 "python" | +| status_types | object | 否 | 状态类型,系统自动分析生成 | +| action_value_mappings | object | 部分 | 动作配置,系统会自动生成一些基础的 | + +### 基本结构示例 + +```yaml +my_device: + class: + module: unilabos.devices.my_module.my_device:MyDevice + type: python + status_types: + status: str + temperature: float + action_value_mappings: + # 动作配置(详见后文) + action_name: + type: UniLabJsonCommand + goal: {...} + result: {...} + + description: "设备描述" + version: "1.0.0" + category: + - device_category + handles: [] + icon: '' + init_param_schema: + config: + properties: + port: + default: DEFAULT_PORT + type: string + required: [] + type: object + data: + properties: + status: + type: string + temperature: + type: number + required: + - status + type: object +``` + +## 创建注册表的方式 + +### 方式1: 使用注册表编辑器(推荐) + +适合大多数场景,快速高效。 + +**步骤**: +1. 启动Uni-Lab +2. 访问Web界面的"注册表编辑器" +3. 上传您的Python设备驱动文件 +4. 点击"分析文件" +5. 填写描述和图标 +6. 点击"生成注册表" +7. 复制生成的YAML内容 +8. 保存到 `unilabos/registry/devices/your_device.yaml` + +### 方式2: 使用--complete_registry参数(开发调试) + +适合开发阶段,自动补全配置。 + +```bash +# 启动时自动补全注册表 +unilab -g dev.json --complete_registry --registry_path ./my_registry +``` + +系统会: +1. 扫描Python类 +2. 分析方法签名和类型 +3. 自动生成缺失的字段 +4. 保存到注册表文件 + +**或者在代码中**: + +```python +# 启动系统时使用参数 +启动系统时用 complete_registry=True 参数,让系统自动补全 +``` + +### 方式3: 手动编写(高级) + +适合需要精细控制或特殊需求的场景。 + +**最小化配置示例**: + +```yaml +# devices/my_device.yaml +my_device: + class: + module: unilabos.devices.my_module.my_device:MyDevice + type: python +``` + +然后启动时使用 `--complete_registry` 让系统自动补全其余内容。 + +## action_value_mappings 详解 + +这个部分定义设备能做哪些动作。系统会自动生成大部分动作,你通常只需要添加特殊的自定义动作。 + +### 系统自动生成的动作 + +1. **以 `auto-` 开头的动作**:从 Python 类的方法自动生成 +2. **通用的驱动动作**: + - `_execute_driver_command`:同步执行驱动命令(仅本地可用) + - `_execute_driver_command_async`:异步执行驱动命令(仅本地可用) + +### 动作配置字段 + +| 字段名 | 需要手写 | 说明 | +| ---------------- | -------- | -------------------------------- | +| type | 是 | 动作类型,必须指定 | +| goal | 是 | 输入参数映射 | +| feedback | 否 | 实时反馈,通常为空 | +| result | 是 | 结果返回映射 | +| goal_default | 部分 | 参数默认值,ROS 动作会自动生成 | +| schema | 部分 | 前端表单配置,ROS 动作会自动生成 | +| handles | 否 | 连接关系,默认为空 | +| placeholder_keys | 否 | 特殊输入字段配置 | + +### 动作类型 + +| 类型 | 使用场景 | 系统自动生成内容 | +| ---------------------- | -------------------- | ---------------------- | +| UniLabJsonCommand | 自定义同步 JSON 命令 | 无 | +| UniLabJsonCommandAsync | 自定义异步 JSON 命令 | 无 | +| ROS 动作类型 | 标准 ROS 动作 | goal_default 和 schema | + +**常用的 ROS 动作类型**: +- `SendCmd`:发送简单命令 +- `NavigateThroughPoses`:导航动作 +- `SingleJointPosition`:单关节位置控制 +- `Stir`:搅拌动作 +- `HeatChill`、`HeatChillStart`:加热冷却动作 + +### 动作命名建议 + +根据设备用途来起名字: + +- **启动停止类**:`start`、`stop`、`pause`、`resume` +- **设置参数类**:`set_speed`、`set_temperature`、`set_timer` +- **移动控制类**:`move_to_position`、`move_through_points` +- **功能操作类**:`stir`、`heat_chill_start`、`heat_chill_stop` +- **开关控制类**:`valve_open_cmd`、`valve_close_cmd`、`push_to` +- **命令执行类**:`send_nav_task`、`execute_command_from_outer` + +### 动作配置示例 + +```yaml +heat_chill_start: + type: HeatChillStart + goal: + purpose: purpose + temp: temp + goal_default: + purpose: '' + temp: 0.0 + handles: + output: + - handler_key: labware + label: Labware + data_type: resource + data_source: handle + data_key: liquid + placeholder_keys: + purpose: unilabos_resources + result: + status: status + success: success + schema: + description: '启动加热冷却功能' + properties: + goal: + properties: + purpose: + type: string + description: '用途说明' + temp: + type: number + description: '目标温度' + required: + - purpose + - temp + title: HeatChillStart_Goal + type: object + required: + - goal + title: HeatChillStart + type: object + feedback: {} +``` + +## 特殊类型的自动识别 + +### ResourceSlot和DeviceSlot识别 + +当您在驱动代码中使用这些特殊类型时,系统会自动识别并生成相应的前端选择器。 + +**Python驱动代码示例**: + +```python +from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot +from typing import List + +class MyDevice: + def test_resource( + self, + resource: ResourceSlot, # 单个资源 + resources: List[ResourceSlot], # 多个资源 + device: DeviceSlot, # 单个设备 + devices: List[DeviceSlot] # 多个设备 + ): + pass +``` + +**自动生成的注册表**(使用--complete_registry): + +```yaml +my_device: + class: + action_value_mappings: + test_resource: + type: UniLabJsonCommand + goal: + resource: resource + resources: resources + device: device + devices: devices + placeholder_keys: + resource: unilabos_resources # 自动添加! + resources: unilabos_resources # 自动添加! + device: unilabos_devices # 自动添加! + devices: unilabos_devices # 自动添加! + result: + success: success +``` + +### 识别规则 + +| Python类型 | placeholder_keys值 | 前端效果 | +|-----------|-------------------|---------| +| `ResourceSlot` | `unilabos_resources` | 单选资源下拉框 | +| `List[ResourceSlot]` | `unilabos_resources` | 多选资源下拉框 | +| `DeviceSlot` | `unilabos_devices` | 单选设备下拉框 | +| `List[DeviceSlot]` | `unilabos_devices` | 多选设备下拉框 | + +### 前端UI效果 + +#### 单选资源 + +```yaml +placeholder_keys: + source: unilabos_resources +``` + +**前端渲染**: +``` +Source: [下拉选择框 ▼] + ├── plate_1 (96孔板) + ├── tiprack_1 (枪头架) + ├── reservoir_1 (试剂槽) + └── ... +``` + +#### 多选资源 + +```yaml +placeholder_keys: + targets: unilabos_resources +``` + +**前端渲染**: +``` +Targets: [多选下拉框 ▼] + ☑ plate_1 (96孔板) + ☐ plate_2 (384孔板) + ☑ plate_3 (96孔板) + └── ... +``` + +#### 单选设备 + +```yaml +placeholder_keys: + pump: unilabos_devices +``` + +**前端渲染**: +``` +Pump: [下拉选择框 ▼] + ├── pump_1 (注射泵A) + ├── pump_2 (注射泵B) + └── ... +``` + +#### 多选设备 + +```yaml +placeholder_keys: + sync_devices: unilabos_devices +``` + +**前端渲染**: +``` +Sync Devices: [多选下拉框 ▼] + ☑ heater_1 (加热器A) + ☑ stirrer_1 (搅拌器) + ☐ pump_1 (注射泵) +``` + +### 手动配置placeholder_keys + +如果需要手动添加或覆盖自动生成的placeholder_keys: + +#### 场景1: 非标准参数名 + +```yaml +action_value_mappings: + custom_action: + goal: + my_custom_resource_param: resource_param + my_device_param: device_param + placeholder_keys: + my_custom_resource_param: unilabos_resources + my_device_param: unilabos_devices +``` + +#### 场景2: 混合类型 + +```python +def mixed_params( + self, + resource: ResourceSlot, + normal_param: str, + device: DeviceSlot +): + pass +``` + +```yaml +placeholder_keys: + resource: unilabos_resources # 资源选择 + device: unilabos_devices # 设备选择 + # normal_param不需要placeholder_keys +``` + +#### 场景3: 自定义选择器 + +```yaml +placeholder_keys: + special_param: custom_selector # 使用自定义选择器 +``` + +## 系统自动生成的字段 + +### status_types + +系统会扫描你的 Python 类,从状态方法(property或get_方法)自动生成这部分: + +```yaml +status_types: + current_temperature: float # 从 get_current_temperature() 或 @property current_temperature + is_heating: bool # 从 get_is_heating() 或 @property is_heating + status: str # 从 get_status() 或 @property status +``` + +**注意事项**: +- 系统会查找所有 `get_` 开头的方法和 `@property` 装饰的属性 +- 类型会自动转成相应的类型(如 `str`、`float`、`bool`) +- 如果类型是 `Any`、`None` 或未知的,默认使用 `String` + +### init_param_schema + +完全由系统自动生成,无需手动编写: + +```yaml +init_param_schema: + config: # 从 __init__ 方法分析得出 + properties: + port: + type: string + default: '/dev/ttyUSB0' + baudrate: + type: integer + default: 9600 + required: [] + type: object + + data: # 根据 status_types 生成的前端类型定义 + properties: + current_temperature: + type: number + is_heating: + type: boolean + status: + type: string + required: + - status + type: object +``` + +**生成规则**: +- `config` 部分:分析 `__init__` 方法的参数、类型和默认值 +- `data` 部分:根据 `status_types` 生成前端显示用的类型定义 + +### 其他自动填充的字段 + +```yaml +version: '1.0.0' # 默认版本 +category: ['文件名'] # 使用 yaml 文件名作为类别 +description: '' # 默认为空 +icon: '' # 默认为空 +handles: [] # 默认空数组 +config_info: [] # 默认空数组 +file_path: '/path/to/file' # 系统自动填写 +registry_type: 'device' # 自动设为设备类型 +``` + +### handles 字段 + +定义设备连接关系: + +```yaml +handles: # 大多数情况为空,除非设备需要特定连接 + - handler_key: device_output + label: Device Output + data_type: resource + data_source: value + data_key: default_value +``` + +### 可选配置字段 + +```yaml +description: '设备的详细描述' + +icon: 'device_icon.webp' # 设备图标文件名(会上传到OSS) + +version: '0.0.1' # 版本号 + +category: # 设备分类,前端用于分组显示 + - 'heating' + - 'cooling' + - 'temperature_control' + +config_info: # 嵌套配置,用于包含子设备 + - children: + - opentrons_24_tuberack_nest_1point5ml_snapcap_A1 + - other_nested_component +``` + +## 完整示例 + +### Python驱动代码 + +```python +# unilabos/devices/my_lab/liquid_handler.py + +from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot +from typing import List, Dict, Any, Optional + +class AdvancedLiquidHandler: + """高级液体处理工作站""" + + def __init__(self, config: Dict[str, Any]): + self.simulation = config.get('simulation', False) + self._status = "idle" + self._temperature = 25.0 + + @property + def status(self) -> str: + """设备状态""" + return self._status + + @property + def temperature(self) -> float: + """当前温度""" + return self._temperature + + def transfer( + self, + source: ResourceSlot, + target: ResourceSlot, + volume: float, + tip: Optional[ResourceSlot] = None + ) -> Dict[str, Any]: + """转移液体""" + return {"success": True} + + def multi_transfer( + self, + source: ResourceSlot, + targets: List[ResourceSlot], + volumes: List[float] + ) -> Dict[str, Any]: + """多目标转移""" + return {"success": True} + + def coordinate_with_heater( + self, + plate: ResourceSlot, + heater: DeviceSlot, + temperature: float + ) -> Dict[str, Any]: + """与加热器协同""" + return {"success": True} +``` + +### 生成的完整注册表 + +```yaml +# unilabos/registry/devices/advanced_liquid_handler.yaml + +advanced_liquid_handler: + class: + module: unilabos.devices.my_lab.liquid_handler:AdvancedLiquidHandler + type: python + + # 自动提取的状态类型 + status_types: + status: str + temperature: float + + # 自动生成的初始化参数 + init_param_schema: + config: + properties: + simulation: + type: boolean + default: false + type: object + data: + properties: + status: + type: string + temperature: + type: number + required: + - status + type: object + + # 动作映射 + action_value_mappings: + transfer: + type: UniLabJsonCommand + goal: + source: source + target: target + volume: volume + tip: tip + goal_default: + source: {} + target: {} + volume: 0.0 + tip: null + placeholder_keys: + source: unilabos_resources # 自动添加 + target: unilabos_resources # 自动添加 + tip: unilabos_resources # 自动添加 + result: + success: success + schema: + description: "转移液体" + properties: + goal: + properties: + source: + type: object + description: "源容器" + target: + type: object + description: "目标容器" + volume: + type: number + description: "体积(μL)" + tip: + type: object + description: "枪头(可选)" + required: + - source + - target + - volume + type: object + required: + - goal + type: object + + multi_transfer: + type: UniLabJsonCommand + goal: + source: source + targets: targets + volumes: volumes + placeholder_keys: + source: unilabos_resources # 单选 + targets: unilabos_resources # 多选 + result: + success: success + + coordinate_with_heater: + type: UniLabJsonCommand + goal: + plate: plate + heater: heater + temperature: temperature + placeholder_keys: + plate: unilabos_resources # 资源选择 + heater: unilabos_devices # 设备选择 + result: + success: success + + description: "高级液体处理工作站,支持多目标转移和设备协同" + version: "1.0.0" + category: + - liquid_handling + handles: [] + icon: "" +``` + +### 另一个完整示例:温度控制器 + +```yaml +my_temperature_controller: + class: + action_value_mappings: + heat_start: + type: HeatChillStart + goal: + target_temp: temp + vessel: vessel + goal_default: + target_temp: 25.0 + vessel: '' + handles: + output: + - handler_key: heated_sample + label: Heated Sample + data_type: resource + data_source: handle + data_key: sample + placeholder_keys: + vessel: unilabos_resources + result: + status: status + success: success + schema: + description: '启动加热功能' + properties: + goal: + properties: + target_temp: + type: number + description: '目标温度' + vessel: + type: string + description: '容器标识' + required: + - target_temp + - vessel + title: HeatStart_Goal + type: object + required: + - goal + title: HeatStart + type: object + feedback: {} + + stop: + type: UniLabJsonCommand + goal: {} + goal_default: {} + handles: {} + result: + status: status + schema: + description: '停止设备' + properties: + goal: + type: object + title: Stop_Goal + title: Stop + type: object + feedback: {} + + module: unilabos.devices.temperature.my_controller:MyTemperatureController + status_types: + current_temperature: float + target_temperature: float + is_heating: bool + is_cooling: bool + status: str + vessel: str + type: python + + description: '我的温度控制器设备' + handles: [] + icon: 'temperature_controller.webp' + init_param_schema: + config: + properties: + port: + default: '/dev/ttyUSB0' + type: string + baudrate: + default: 9600 + type: number + required: [] + type: object + data: + properties: + current_temperature: + type: number + target_temperature: + type: number + is_heating: + type: boolean + is_cooling: + type: boolean + status: + type: string + vessel: + type: string + required: + - current_temperature + - target_temperature + - status + type: object + + version: '1.0.0' + category: + - 'temperature_control' + - 'heating' + config_info: [] +``` + +## 部署和使用 + +### Python 驱动类要求 + +你的设备类需要符合以下要求: + +```python +from unilabos.common.device_base import DeviceBase + +class MyDevice(DeviceBase): + def __init__(self, config): + """初始化,参数会自动分析到 init_param_schema.config""" + super().__init__(config) + self.port = config.get('port', '/dev/ttyUSB0') + + # 状态方法(会自动生成到 status_types) + @property + def status(self): + """返回设备状态""" + return "idle" + + @property + def temperature(self): + """返回当前温度""" + return 25.0 + + # 动作方法(会自动生成 auto- 开头的动作) + async def start_heating(self, temperature: float): + """开始加热到指定温度""" + pass + + def stop(self): + """停止操作""" + pass +``` + +### 方法一:使用编辑器(推荐) + +1. 先编写 Python 驱动类 +2. 使用注册表编辑器自动生成 yaml 配置 +3. 保存生成的文件到 `devices/` 目录 +4. 重启 UniLabOS 即可使用 + +### 方法二:手动编写(简化版) + +1. 创建最小配置: + +```yaml +# devices/my_device.yaml +my_device: + class: + module: unilabos.devices.my_module.my_device:MyDevice + type: python +``` + +2. 启动系统时使用 `--complete_registry` 参数,让系统自动补全 + +3. 检查生成的配置是否符合预期 + +### 系统集成 + +1. 将 yaml 文件放到 `unilabos/registry/devices/` 目录 +2. 系统启动时会自动扫描并加载设备 +3. 系统会自动补全所有缺失的字段 +4. 设备即可在前端界面中使用 + +### 高级配置 + +如果需要特殊设置,可以手动添加: + +```yaml +my_device: + class: + module: unilabos.devices.my_module.my_device:MyDevice + type: python + action_value_mappings: + # 自定义动作 + special_command: + type: UniLabJsonCommand + goal: {} + result: {} + + # 可选的自定义配置 + description: '我的特殊设备' + icon: 'my_device.webp' + category: ['temperature', 'heating'] +``` + +## 调试和验证 + +### 1. 检查生成的注册表 + +```bash +# 使用complete_registry生成 +unilab -g dev.json --complete_registry + +# 查看生成的文件 +cat unilabos/registry/devices/my_device.yaml +``` + +### 2. 验证placeholder_keys + +确认: +- ResourceSlot参数有 `unilabos_resources` +- DeviceSlot参数有 `unilabos_devices` +- List类型被正确识别 + +### 3. 测试前端效果 + +1. 启动Uni-Lab +2. 访问Web界面 +3. 选择设备 +4. 调用动作 +5. 检查是否显示正确的选择器 + +### 4. 检查文件路径和导入 + +```bash +# 确认模块路径正确 +python -c "from unilabos.devices.my_module.my_device import MyDevice" +``` + +## 常见问题 + +### Q1: placeholder_keys没有自动生成 + +**检查**: +1. 是否使用了`--complete_registry`参数? +2. 类型注解是否正确? + ```python + # ✓ 正确 + def method(self, resource: ResourceSlot): + + # ✗ 错误(缺少类型注解) + def method(self, resource): + ``` +3. 是否正确导入? + ```python + from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot + ``` + +### Q2: 前端显示普通输入框而不是选择器 + +**原因**: placeholder_keys未正确配置 + +**解决**: +```yaml +# 检查YAML中是否有 +placeholder_keys: + resource: unilabos_resources +``` + +### Q3: 多选不工作 + +**检查类型注解**: +```python +# ✓ 正确 - 会生成多选 +def method(self, resources: List[ResourceSlot]): + +# ✗ 错误 - 会生成单选 +def method(self, resources: ResourceSlot): +``` + +### Q4: 运行时收到错误的类型 + +**说明**: 运行时会自动转换 + +前端传递: +```json +{ + "resource": "plate_1" // 字符串ID +} +``` + +运行时收到: +```python +resource.id # "plate_1" +resource.name # "96孔板" +resource.type # "resource" +# 完整的Resource对象 +``` + +### Q5: 设备加载不了 + +**检查**: +1. 确认 `class.module` 路径是否正确 +2. 确认 Python 驱动类能否正常导入 +3. 使用 yaml 验证器检查文件格式 +4. 查看 UniLabOS 启动日志中的错误信息 + +### Q6: 自动生成失败 + +**检查**: +1. 确认类继承了正确的基类 +2. 确保状态方法的返回类型注解清晰 +3. 检查类能否被动态导入 +4. 确认启用了 `complete_registry=True` + +### Q7: 前端显示问题 + +**解决步骤**: +1. 删除旧的 yaml 文件,用编辑器重新生成 +2. 清除浏览器缓存,重新加载页面 +3. 确认必需字段(如 `schema`)都存在 +4. 检查 `goal_default` 和 `schema` 的数据类型是否一致 + +### Q8: 动作执行出错 + +**检查**: +1. 确认动作方法名符合规范(如 `execute_`) +2. 检查 `goal` 字段的参数映射是否正确 +3. 确认方法返回值格式符合 `result` 映射 +4. 在驱动类中添加异常处理 + +## 最佳实践 + +### 开发流程 + +1. **优先使用编辑器**:除非有特殊需求,否则优先使用注册表编辑器 +2. **最小化配置**:手动配置时只定义必要字段,让系统自动生成其他内容 +3. **增量开发**:先创建基本配置,后续根据需要添加特殊动作 +4. **及时测试**:每次修改后及时在开发环境测试 + +### 代码规范 + +1. **始终使用类型注解** + +```python +# ✓ 好 +def method(self, resource: ResourceSlot, device: DeviceSlot): + pass + +# ✗ 差 +def method(self, resource, device): + pass +``` + +2. **提供有意义的参数名** + +```python +# ✓ 好 - 清晰的参数名 +def transfer(self, source: ResourceSlot, target: ResourceSlot): + pass + +# ✗ 差 - 模糊的参数名 +def transfer(self, r1: ResourceSlot, r2: ResourceSlot): + pass +``` + +3. **使用Optional表示可选参数** + +```python +from typing import Optional + +def method( + self, + required_resource: ResourceSlot, + optional_resource: Optional[ResourceSlot] = None +): + pass +``` + +4. **添加详细的文档字符串** + +```python +def method( + self, + source: ResourceSlot, # 源容器 + targets: List[ResourceSlot] # 目标容器列表 +) -> Dict[str, Any]: + """方法说明 + + Args: + source: 源容器,必须包含足够的液体 + targets: 目标容器列表,每个容器应该为空 + + Returns: + 包含操作结果的字典 + """ + pass +``` + +5. **方法命名规范** + - 状态方法使用 `@property` 装饰器或 `get_` 前缀 + - 动作方法使用动词开头 + - 保持命名清晰、一致 + +6. **完善的错误处理** + - 实现完善的错误处理 + - 添加日志记录 + - 提供有意义的错误信息 + +### 配置管理 + +1. **版本控制**:所有 yaml 文件纳入版本控制 +2. **命名一致性**:设备 ID、文件名、类名保持一致的命名风格 +3. **定期更新**:定期运行完整注册以更新自动生成的字段 +4. **备份配置**:在修改前备份重要的手动配置 +5. **文档同步**:保持配置文件和文档的同步更新 + +### 测试验证 + +1. **本地测试**:在本地环境充分测试后再部署 +2. **渐进部署**:先部署到测试环境,验证无误后再上生产环境 +3. **监控日志**:密切监控设备加载和运行日志 +4. **回滚准备**:准备快速回滚机制,以应对紧急情况 +5. **自动化测试**:编写单元测试和集成测试 + +### 性能优化 + +1. **按需加载**:只加载实际使用的设备类型 +2. **缓存利用**:充分利用系统的注册表缓存机制 +3. **资源管理**:合理管理设备连接和资源占用 +4. **监控指标**:设置关键性能指标的监控和告警 + +## 参考资料 + +- {doc}`add_device` - 设备驱动编写指南 +- {doc}`04_add_device_testing` - 设备测试指南 +- Python [typing模块](https://docs.python.org/3/library/typing.html) +- [YAML语法](https://yaml.org/) +- [JSON Schema](https://json-schema.org/) + + diff --git a/docs/developer_guide/add_batteryPLC.md b/docs/developer_guide/examples/battery_plc_workstation.md similarity index 61% rename from docs/developer_guide/add_batteryPLC.md rename to docs/developer_guide/examples/battery_plc_workstation.md index f1e93974..e27e4df1 100644 --- a/docs/developer_guide/add_batteryPLC.md +++ b/docs/developer_guide/examples/battery_plc_workstation.md @@ -1,6 +1,17 @@ -# 电池装配工站接入(PLC) +# 实例:电池装配工站接入(PLC控制) -本指南将引导你完成电池装配工站(以 PLC 控制为例)的接入流程,包括新建工站文件、编写驱动与寄存器读写、生成注册表、上传及注意事项。 +> **文档类型**:实际应用案例 +> **适用场景**:使用 PLC 控制的电池装配工站接入 +> **前置知识**:{doc}`../add_device` | {doc}`../add_registry` + +本指南以电池装配工站为实际案例,引导你完成 PLC 控制设备的完整接入流程,包括新建工站文件、编写驱动与寄存器读写、生成注册表、上传及注意事项。 + +## 案例概述 + +**设备类型**:电池装配工站 +**通信方式**:Modbus TCP (PLC) +**工站基类**:`WorkstationBase` +**主要功能**:电池组装、寄存器读写、数据采集 ## 1. 新建工站文件 @@ -93,10 +104,12 @@ python unilabos\app\main.py -g celljson.json --ak --sk ``` 点击注册表编辑,进入注册表编辑页面 -![Layers](image_add_batteryPLC/unilab_sys_status.png) + +![系统状态页面](image_battery_plc/unilab_sys_status.png) 按照图示步骤填写自动生成注册表信息: -![Layers](image_add_batteryPLC/unilab_registry_process.png) + +![注册表生成流程](image_battery_plc/unilab_registry_process.png) 步骤说明: 1. 选择新增的工站`coin_cell_assembly.py`文件 @@ -107,8 +120,9 @@ python unilabos\app\main.py -g celljson.json --ak --sk 6. 填写新的工站注册表备注信息 7. 生成注册表 -以上操作步骤完成,则会生成的新的注册表ymal文件,如下图: -![Layers](image_add_batteryPLC/unilab_new_yaml.png) +以上操作步骤完成,则会生成的新的注册表YAML文件,如下图: + +![生成的YAML文件](image_battery_plc/unilab_new_yaml.png) @@ -134,14 +148,60 @@ python unilabos\app\main.py -g celljson.json --ak --sk - ## 4. 注意事项 -- 在新生成的 YAML 中,确认 `module` 指向新工站类,本例中需检查`coincellassemblyworkstation_device.yaml`文件中是否指向了`coin_cell_assembly.py`文件中定义的`CoinCellAssemblyWorkstation`类文件: +### 4.1 验证模块路径 -``` +在新生成的 YAML 中,确认 `module` 指向新工站类。本例中需检查 `coincellassemblyworkstation_device.yaml` 文件中是否正确指向了 `CoinCellAssemblyWorkstation` 类: + +```yaml module: unilabos.devices.workstation.coin_cell_assembly.coin_cell_assembly:CoinCellAssemblyWorkstation ``` -- 首次新增设备(或资源)需要在网页端新增注册表信息,`--complete_registry`补全注册表,`--upload_registry`上传注册表信息。 +### 4.2 首次接入流程 -- 如果不是新增设备(或资源),仅对工站驱动的.py文件进行了修改,则不需要在网页端新增注册表信息。只需要运行补全注册表信息之后,上传注册表即可。 +首次新增设备(或资源)需要完整流程: +1. ✅ 在网页端生成注册表信息 +2. ✅ 使用 `--complete_registry` 补全注册表 +3. ✅ 使用 `--upload_registry` 上传注册表信息 + +### 4.3 驱动更新流程 + +如果不是新增设备,仅修改了工站驱动的 `.py` 文件: +1. ✅ 运行 `--complete_registry` 补全注册表 +2. ✅ 运行 `--upload_registry` 上传注册表 +3. ❌ 不需要在网页端重新生成注册表 + +### 4.4 PLC通信注意事项 + +- **握手机制**:若需参数下发,建议在 PLC 端设置标志寄存器并完成握手复位,避免粘连与竞争 +- **字节序**:FLOAT32 等多字节数据类型需要正确指定字节序(如 `WorderOrder.LITTLE`) +- **寄存器映射**:确保 CSV 文件中的寄存器地址与 PLC 实际配置一致 +- **连接稳定性**:在初始化时检查 PLC 连接状态,建议添加重连机制 + +## 5. 扩展阅读 + +### 相关文档 + +- {doc}`../add_device` - 设备驱动编写通用指南 +- {doc}`../add_registry` - 注册表配置完整指南 +- {doc}`../workstation_architecture` - 工站架构详解 + +### 技术要点 + +- **Modbus TCP 通信**:PLC 通信协议和寄存器读写 +- **WorkstationBase**:工站基类的继承和使用 +- **寄存器映射**:CSV 格式的寄存器配置 +- **注册表生成**:自动化工具使用 + +## 6. 总结 + +通过本案例,你应该掌握: + +1. ✅ 如何创建 PLC 控制的工站驱动 +2. ✅ Modbus TCP 通信和寄存器读写 +3. ✅ 使用可视化编辑器生成注册表 +4. ✅ 注册表的补全和上传流程 +5. ✅ 新增设备与更新驱动的区别 + +这个案例展示了完整的 PLC 设备接入流程,可以作为其他类似设备接入的参考模板。 diff --git a/docs/developer_guide/image_add_batteryPLC/unilab_new_yaml.png b/docs/developer_guide/examples/image_battery_plc/unilab_new_yaml.png similarity index 100% rename from docs/developer_guide/image_add_batteryPLC/unilab_new_yaml.png rename to docs/developer_guide/examples/image_battery_plc/unilab_new_yaml.png diff --git a/docs/developer_guide/image_add_batteryPLC/unilab_registry_process.png b/docs/developer_guide/examples/image_battery_plc/unilab_registry_process.png similarity index 100% rename from docs/developer_guide/image_add_batteryPLC/unilab_registry_process.png rename to docs/developer_guide/examples/image_battery_plc/unilab_registry_process.png diff --git a/docs/developer_guide/image_add_batteryPLC/unilab_sys_status.png b/docs/developer_guide/examples/image_battery_plc/unilab_sys_status.png similarity index 100% rename from docs/developer_guide/image_add_batteryPLC/unilab_sys_status.png rename to docs/developer_guide/examples/image_battery_plc/unilab_sys_status.png diff --git a/docs/developer_guide/materials_construction_guide.md b/docs/developer_guide/examples/materials_construction_guide.md similarity index 98% rename from docs/developer_guide/materials_construction_guide.md rename to docs/developer_guide/examples/materials_construction_guide.md index a19fa05c..90f23905 100644 --- a/docs/developer_guide/materials_construction_guide.md +++ b/docs/developer_guide/examples/materials_construction_guide.md @@ -1,4 +1,8 @@ -# 物料构建指南 +# 实例:物料构建指南 + +> **文档类型**:物料系统实战指南 +> **适用场景**:工作站物料系统构建、Deck/Warehouse/Carrier/Bottle 配置 +> **前置知识**:PyLabRobot 基础 | 资源管理概念 ## 概述 diff --git a/docs/developer_guide/materials_tutorial.md b/docs/developer_guide/examples/materials_tutorial.md similarity index 96% rename from docs/developer_guide/materials_tutorial.md rename to docs/developer_guide/examples/materials_tutorial.md index 7142b270..398d2d86 100644 --- a/docs/developer_guide/materials_tutorial.md +++ b/docs/developer_guide/examples/materials_tutorial.md @@ -1,6 +1,10 @@ -# 物料教程(Resource) +# 实例:物料教程(Resource) -本教程面向 Uni-Lab-OS 的开发者,讲解“物料”的核心概念、3种物料格式(UniLab、PyLabRobot、奔耀Bioyond)及其相互转换方法,并说明4种 children 结构表现形式及使用场景。 +> **文档类型**:物料系统完整教程 +> **适用场景**:物料格式转换、多系统物料对接、资源结构理解 +> **前置知识**:Python 基础 | JSON 数据结构 + +本教程面向 Uni-Lab-OS 的开发者,讲解"物料"的核心概念、3种物料格式(UniLab、PyLabRobot、奔耀Bioyond)及其相互转换方法,并说明4种 children 结构表现形式及使用场景。 --- diff --git a/docs/developer_guide/workstation_architecture.md b/docs/developer_guide/examples/workstation_architecture.md similarity index 97% rename from docs/developer_guide/workstation_architecture.md rename to docs/developer_guide/examples/workstation_architecture.md index 073d9aea..52f1966f 100644 --- a/docs/developer_guide/workstation_architecture.md +++ b/docs/developer_guide/examples/workstation_architecture.md @@ -1,4 +1,8 @@ -# 工作站模板架构设计与对接指南 +# 实例:工作站模板架构设计与对接指南 + +> **文档类型**:架构设计指南与实战案例 +> **适用场景**:大型工作站接入、子设备管理、物料系统集成 +> **前置知识**:{doc}`../add_device` | {doc}`../add_registry` ## 0. 问题简介 @@ -6,9 +10,9 @@ ### 0.1 自研常量有机工站:最重要的是子设备管理和通信转发 -![workstation_organic_yed](image/workstation_architecture/workstation_organic_yed.png) +![workstation_organic_yed](../image/workstation_architecture/workstation_organic_yed.png) -![workstation_organic](image/workstation_architecture/workstation_organic.png) +![workstation_organic](../image/workstation_architecture/workstation_organic.png) 这类工站由开发者自研,组合所有子设备和实验耗材、希望让他们在工作站这一级协调配合; @@ -18,7 +22,7 @@ ### 0.2 移液工作站:物料系统和工作流模板管理 -![workstation_liquid_handler](image/workstation_architecture/workstation_liquid_handler.png) +![workstation_liquid_handler](../image/workstation_architecture/workstation_liquid_handler.png) 1. 绝大多数情况没有子设备,有时候选配恒温震荡等模块时,接口也由工作站提供 2. 所有任务系统均由工作站本身实现并下发指令,有统一的抽象函数可实现(pick_up_tips, aspirate, dispense, transfer 等)。有时需要将这些指令组合、转化为工作站的脚本语言,再统一下发。因此会形成大量固定的 protocols。 @@ -26,7 +30,7 @@ ### 0.3 厂家开发的定制大型工站 -![workstation_by_supplier](image/workstation_architecture/workstation_by_supplier.png) +![workstation_by_supplier](../image/workstation_architecture/workstation_by_supplier.png) 由厂家开发,具备完善的物料系统、任务系统甚至调度系统;由 PLC 或 OpenAPI TCP 协议统一通信 diff --git a/docs/developer_guide/networking_overview.md b/docs/developer_guide/networking_overview.md new file mode 100644 index 00000000..e2cda520 --- /dev/null +++ b/docs/developer_guide/networking_overview.md @@ -0,0 +1,595 @@ +# 组网部署与主从模式配置 + +本文档介绍 Uni-Lab-OS 的组网架构、部署方式和主从模式的详细配置。 + +## 目录 + +- [架构概览](#架构概览) +- [节点类型](#节点类型) +- [通信机制](#通信机制) +- [典型拓扑](#典型拓扑) +- [主从模式配置](#主从模式配置) +- [网络配置](#网络配置) +- [示例:多房间部署](#示例多房间部署) +- [故障处理](#故障处理) +- [监控和维护](#监控和维护) + +--- + +## 架构概览 + +Uni-Lab-OS 支持多种部署模式: + +``` +┌──────────────────────────────────────────────┐ +│ Cloud Platform/Self-hosted Platform │ +│ uni-lab.bohrium.com │ +│ (Resource Management, Task Scheduling, │ +│ Monitoring) │ +└────────────────────┬─────────────────────────┘ + │ WebSocket / HTTP + │ + ┌──────────┴──────────┐ + │ │ + ┌────▼─────┐ ┌────▼─────┐ + │ Master │◄──ROS2──►│ Slave │ + │ Node │ │ Node │ + │ (Host) │ │ (Slave) │ + └────┬─────┘ └────┬─────┘ + │ │ + ┌────┴────┐ ┌────┴────┐ + │ Device A│ │ Device B│ + │ Device C│ │ Device D│ + └─────────┘ └─────────┘ +``` + +--- + +## 节点类型 + +### 主节点(Host Node) + +**功能**: + +- 创建和管理全局资源 +- 提供 host_node 服务 +- 连接云端平台 +- 协调多个从节点 +- 提供 Web 管理界面 + +**启动命令**: + +```bash +unilab --ak your_ak --sk your_sk -g host_devices.json +``` + +### 从节点(Slave Node) + +**功能**: + +- 管理本地设备 +- 不连接云端(可选) +- 向主节点注册 +- 执行分配的任务 + +**启动命令**: + +```bash +unilab --ak your_ak --sk your_sk -g slave_devices.json --is_slave +``` + +--- + +## 通信机制 + +### ROS2 通信 + +**用途**: 节点间实时通信 + +**通信方式**: + +- **Topic**: 状态广播(设备状态、传感器数据) +- **Service**: 同步请求(资源查询、配置获取) +- **Action**: 异步任务(设备操作、长时间运行) + +**示例**: + +```bash +# 查看ROS2节点 +ros2 node list + +# 查看topic +ros2 topic list + +# 查看action +ros2 action list +``` + +### WebSocket 通信 + +**用途**: 主节点与云端通信 + +**特点**: + +- 实时双向通信 +- 自动重连 +- 心跳保持 + +**配置**: + +```python +# local_config.py +BasicConfig.ak = "your_ak" +BasicConfig.sk = "your_sk" +``` + +--- + +## 典型拓扑 + +### 单节点模式 + +**适用场景**: 小型实验室、开发测试 + +``` +┌──────────────────┐ +│ Uni-Lab Node │ +│ ┌────────────┐ │ +│ │ Device A │ │ +│ │ Device B │ │ +│ │ Device C │ │ +│ └────────────┘ │ +└──────────────────┘ +``` + +**优点**: + +- 配置简单 +- 无网络延迟 +- 适合快速原型 + +**启动**: + +```bash +unilab --ak your_ak --sk your_sk -g all_devices.json +``` + +### 主从模式 + +**适用场景**: 多房间、分布式设备 + +``` +┌─────────────┐ ┌──────────────┐ +│ Master Node │◄────►│ Slave Node 1 │ +│ Coordinator │ │ Liquid │ +│ Web UI │ │ Handling │ +└──────┬──────┘ └──────────────┘ + │ + │ ┌──────────────┐ + └────────────►│ Slave Node 2 │ + │ Analytical │ + │ (NMR/GC) │ + └──────────────┘ +``` + +**优点**: + +- 物理分隔 +- 独立故障域 +- 易于扩展 + +**适用场景**: + +- 设备物理位置分散 +- 不同房间的设备 +- 需要独立故障域 +- 分阶段扩展系统 + +**主节点**: + +```bash +unilab --ak your_ak --sk your_sk -g host.json +``` + +**从节点**: + +```bash +unilab --ak your_ak --sk your_sk -g slave1.json --is_slave +unilab --ak your_ak --sk your_sk -g slave2.json --is_slave --port 8003 +``` + +### 云端集成模式 + +**适用场景**: 远程监控、多实验室协作 + +``` + Cloud Platform + │ + ┌───────┴────────┐ + │ │ +Laboratory A Laboratory B +(Master Node) (Master Node) +``` + +**优点**: + +- 远程访问 +- 数据同步 +- 任务调度 + +**启动**: + +```bash +# 实验室A +unilab --ak your_ak --sk your_sk --upload_registry --use_remote_resource + +# 实验室B +unilab --ak your_ak --sk your_sk --upload_registry --use_remote_resource +``` + +--- + +## 主从模式配置 + +### 主节点配置 + +#### 1. 创建主节点设备图 + +`host.json`: + +```json +{ + "nodes": [], + "links": [] +} +``` + +#### 2. 启动主节点 + +```bash +# 基本启动 +unilab --ak your_ak --sk your_sk -g host.json + +# 带云端集成 +unilab --ak your_ak --sk your_sk -g host.json --upload_registry + +# 指定端口 +unilab --ak your_ak --sk your_sk -g host.json --port 8002 +``` + +#### 3. 验证主节点 + +```bash +# 检查ROS2节点 +ros2 node list +# 应该看到 /host_node + +# 检查服务 +ros2 service list | grep host_node + +# Web界面 +# 访问 http://localhost:8002 +``` + +### 从节点配置 + +#### 1. 创建从节点设备图 + +`slave1.json`: + +```json +{ + "nodes": [ + { + "id": "liquid_handler_1", + "name": "液体处理工作站", + "type": "device", + "class": "liquid_handler", + "config": { + "simulation": false + } + } + ], + "links": [] +} +``` + +#### 2. 启动从节点 + +```bash +# 基本从节点启动 +unilab --ak your_ak --sk your_sk -g slave1.json --is_slave + +# 指定不同端口(如果多个从节点在同一台机器) +unilab --ak your_ak --sk your_sk -g slave1.json --is_slave --port 8003 + +# 跳过等待主节点(独立测试) +unilab --ak your_ak --sk your_sk -g slave1.json --is_slave --slave_no_host +``` + +#### 3. 验证从节点 + +```bash +# 检查节点连接 +ros2 node list + +# 检查设备状态 +ros2 topic echo /liquid_handler_1/status +``` + +### 跨节点通信 + +#### 资源访问 + +主节点可以访问从节点的资源: + +```bash +# 在主节点或其他节点调用从节点设备 +ros2 action send_goal /liquid_handler_1/transfer_liquid \ + unilabos_msgs/action/TransferLiquid \ + "{source: {...}, target: {...}, volume: 100.0}" +``` + +#### 状态监控 + +主节点监控所有从节点状态: + +```bash +# 订阅从节点状态 +ros2 topic echo /liquid_handler_1/status + +# 查看所有设备状态 +ros2 topic list | grep status +``` + +--- + +## 网络配置 + +### ROS2 DDS 配置 + +确保主从节点在同一网络: + +```bash +# 检查网络可达性 +ping + +# 设置ROS_DOMAIN_ID(可选,用于隔离) +export ROS_DOMAIN_ID=42 +``` + +### 防火墙配置 + +**建议做法**: + +为了确保 ROS2 DDS 通信正常,建议直接关闭防火墙,而不是配置特定端口。ROS2 使用动态端口范围,配置特定端口可能导致通信问题。 + +**Linux**: + +```bash +# 关闭防火墙 +sudo ufw disable + +# 或者临时停止防火墙 +sudo systemctl stop ufw +``` + +**Windows**: + +```powershell +# 在Windows安全中心关闭防火墙 +# 控制面板 -> 系统和安全 -> Windows Defender 防火墙 -> 启用或关闭Windows Defender防火墙 +``` + +### 验证网络连通性 + +在配置完成后,使用 ROS2 自带的 demo 节点来验证跨节点通信是否正常: + +**在主节点机器上**(激活 unilab 环境后): + +```bash +# 启动talker +ros2 run demo_nodes_cpp talker + +# 同时在另一个终端启动listener +ros2 run demo_nodes_cpp listener +``` + +**在从节点机器上**(激活 unilab 环境后): + +```bash +# 启动talker +ros2 run demo_nodes_cpp talker + +# 同时在另一个终端启动listener +ros2 run demo_nodes_cpp listener +``` + +**注意**:必须在两台机器上**互相启动** talker 和 listener,否则可能出现只能收不能发的单向通信问题。 + +**预期结果**: + +- 每台机器的 listener 应该能同时接收到本地和远程 talker 发送的消息 +- 如果只能看到本地消息,说明网络配置有问题 +- 如果两台机器都能互相收发消息,则组网配置正确 + +### 本地网络要求 + +**ROS2 通信**: + +- 同一局域网或 VPN +- 端口:默认 DDS 端口(7400-7500) +- 组播支持(或配置 unicast) + +**检查连通性**: + +```bash +# Ping测试 +ping + +# ROS2节点发现 +ros2 node list +ros2 daemon stop && ros2 daemon start +``` + +### 云端连接 + +**要求**: + +- HTTPS (443) +- WebSocket 支持 +- 稳定的互联网连接 + +**测试连接**: + +```bash +# 测试云端连接 +curl https://uni-lab.bohrium.com/api/v1/health + +# 测试WebSocket +# 启动Uni-Lab后查看日志 +``` + +--- + +## 示例:多房间部署 + +### 场景描述 + +- **房间 A**: 主控室,有 Web 界面 +- **房间 B**: 液体处理室 +- **房间 C**: 分析仪器室 + +### 房间 A - 主节点 + +```bash +# host.json +unilab --ak your_ak --sk your_sk -g host.json --port 8002 +``` + +### 房间 B - 从节点 1 + +```bash +# liquid_handler.json +unilab --ak your_ak --sk your_sk -g liquid_handler.json --is_slave --port 8003 +``` + +### 房间 C - 从节点 2 + +```bash +# analytical.json +unilab --ak your_ak --sk your_sk -g analytical.json --is_slave --port 8004 +``` + +--- + +## 故障处理 + +### 节点离线 + +**检测**: + +```bash +ros2 node list # 查看在线节点 +``` + +**处理**: + +1. 检查网络连接 +2. 重启节点 +3. 检查日志 + +### 从节点无法连接主节点 + +1. 检查网络: + + ```bash + ping + ``` + +2. 检查 ROS_DOMAIN_ID: + + ```bash + echo $ROS_DOMAIN_ID + ``` + +3. 使用`--slave_no_host`测试: + ```bash + unilab --ak your_ak --sk your_sk -g slave.json --is_slave --slave_no_host + ``` + +### 通信延迟 + +**排查**: + +```bash +# 网络延迟 +ping + +# ROS2话题延迟 +ros2 topic hz /device_status +ros2 topic bw /device_status +``` + +**优化**: + +- 减少发布频率 +- 使用 QoS 配置 +- 优化网络带宽 + +### 数据同步失败 + +**检查**: + +```bash +# 查看日志 +tail -f unilabos_data/logs/unilab.log | grep sync +``` + +**解决**: + +- 检查云端连接 +- 验证 AK/SK +- 手动触发同步 + +### 资源不可见 + +检查资源注册: + +```bash +ros2 service call /host_node/resource_list \ + unilabos_msgs/srv/ResourceList +``` + +--- + +## 监控和维护 + +### 节点状态监控 + +```bash +# 查看所有节点 +ros2 node list + +# 查看话题 +ros2 topic list +``` + +--- + +## 相关文档 + +- [最佳实践指南](../user_guide/best_practice.md) - 完整的实验室搭建流程 +- [安装指南](../user_guide/installation.md) - 环境安装步骤 +- [启动参数详解](../user_guide/launch.md) - 启动参数说明 +- [添加设备驱动](add_device.md) - 自定义设备开发 +- [工作站架构](workstation_architecture.md) - 复杂工作站搭建 + +--- + +## 参考资料 + +- [ROS2 网络配置](https://docs.ros.org/en/humble/Tutorials/Advanced/Networking.html) +- [DDS 配置](https://fast-dds.docs.eprosima.com/) +- Uni-Lab 云平台文档 + diff --git a/docs/index.md b/docs/index.md index a8bf8252..e795dbcf 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ -# Uni-Lab 项目文档 +# Uni-Lab-OS 项目文档 -欢迎来到项目文档的首页! +Uni-Lab-OS 是一个开源的实验室自动化操作系统,提供统一的设备接口、工作流管理和分布式部署能力。 ```{toctree} :maxdepth: 3 diff --git a/docs/intro.md b/docs/intro.md index 4c1cd4c7..8ef2a1ce 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -10,35 +10,51 @@ concepts/01-communication-instruction.md concepts/02-topology-and-chemputer-compile.md ``` -## **用户指南** +## 用户指南 -本指南将带你了解如何使用项目的功能。 +快速上手、系统配置与使用说明。 ```{toctree} :maxdepth: 2 +user_guide/best_practice.md user_guide/installation.md -user_guide/configuration.md user_guide/launch.md +user_guide/graph_files.md boot_examples/index.md ``` +## 进阶配置 + +高级配置和系统管理。 + +```{toctree} +:maxdepth: 2 + +advanced_usage/configuration.md +advanced_usage/working_directory.md +``` + ## 开发者指南 -```{toctree} +设备开发、系统扩展与架构说明。 +```{toctree} :maxdepth: 2 -developer_guide/device_driver -developer_guide/add_device -developer_guide/add_action -developer_guide/actions -developer_guide/workstation_architecture -developer_guide/add_protocol -developer_guide/add_batteryPLC -developer_guide/materials_tutorial -developer_guide/materials_construction_guide - +developer_guide/networking_overview.md +developer_guide/add_device.md +developer_guide/add_old_device.md +developer_guide/add_registry.md +developer_guide/add_yaml.md +developer_guide/add_action.md +developer_guide/actions.md +developer_guide/action_includes.md +developer_guide/add_protocol.md +developer_guide/examples/workstation_architecture.md +developer_guide/examples/materials_construction_guide.md +developer_guide/examples/materials_tutorial.md +developer_guide/examples/battery_plc_workstation.md ``` ## 接口文档 diff --git a/docs/user_guide/best_practice.md b/docs/user_guide/best_practice.md new file mode 100644 index 00000000..5b960e8e --- /dev/null +++ b/docs/user_guide/best_practice.md @@ -0,0 +1,1837 @@ +# 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_96(96孔板) +│ ├── PlateCarrier_2 +│ │ └── Plate_384(384孔板) +│ └── TipRack(枪头架) +├── Pipette_8(8通道移液器) +└── 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) + + +#### 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 版本:最新稳定版_ diff --git a/docs/user_guide/configuration.md b/docs/user_guide/configuration.md deleted file mode 100644 index 036cb9c7..00000000 --- a/docs/user_guide/configuration.md +++ /dev/null @@ -1,442 +0,0 @@ -# Uni-Lab 配置指南 - -Uni-Lab 支持通过 Python 配置文件进行灵活的系统配置。本指南将帮助您理解配置选项并设置您的 Uni-Lab 环境。 - -## 配置文件格式 - -Uni-Lab 支持 Python 格式的配置文件,它比 YAML 或 JSON 提供更多的灵活性,包括支持注释、条件逻辑和复杂数据结构。 - -### 默认配置示例 - -首次使用时,系统会自动创建一个基础配置文件 `local_config.py`: - -```python -# unilabos的配置文件 - -class BasicConfig: - ak = "" # 实验室网页给您提供的ak代码,您可以在配置文件中指定,也可以通过运行unilabos时以 --ak 传入,优先按照传入参数解析 - sk = "" # 实验室网页给您提供的sk代码,您可以在配置文件中指定,也可以通过运行unilabos时以 --sk 传入,优先按照传入参数解析 - - -# WebSocket配置,一般无需调整 -class WSConfig: - reconnect_interval = 5 # 重连间隔(秒) - max_reconnect_attempts = 999 # 最大重连次数 - ping_interval = 30 # ping间隔(秒) -``` -您可以进入实验室,点击左下角的头像在实验室详情中获取所在实验室的ak sk -![copy_aksk.gif](image/copy_aksk.gif) - -### 完整配置示例 - -您可以根据需要添加更多配置选项: - -```python -#!/usr/bin/env python -# coding=utf-8 -"""Uni-Lab 配置文件""" - -# 基础配置 -class BasicConfig: - ak = "your_access_key" # 实验室访问密钥 - sk = "your_secret_key" # 实验室私钥 - working_dir = "" # 工作目录(通常自动设置) - config_path = "" # 配置文件路径(自动设置) - is_host_mode = True # 是否为主站模式 - slave_no_host = False # 从站模式下是否跳过等待主机服务 - upload_registry = False # 是否上传注册表 - machine_name = "undefined" # 机器名称(自动获取) - vis_2d_enable = False # 是否启用2D可视化 - enable_resource_load = True # 是否启用资源加载 - communication_protocol = "websocket" # 通信协议 - -# WebSocket配置 -class WSConfig: - reconnect_interval = 5 # 重连间隔(秒) - max_reconnect_attempts = 999 # 最大重连次数 - ping_interval = 30 # ping间隔(秒) - -# OSS上传配置 -class OSSUploadConfig: - api_host = "" # API主机地址 - authorization = "" # 授权信息 - init_endpoint = "" # 初始化端点 - complete_endpoint = "" # 完成端点 - max_retries = 3 # 最大重试次数 - -# HTTP配置 -class HTTPConfig: - remote_addr = "http://127.0.0.1:48197/api/v1" # 远程地址 - -# ROS配置 -class ROSConfig: - modules = [ - "std_msgs.msg", - "geometry_msgs.msg", - "control_msgs.msg", - "control_msgs.action", - "nav2_msgs.action", - "unilabos_msgs.msg", - "unilabos_msgs.action", - ] # 需要加载的ROS模块 -``` - -## 命令行参数覆盖配置 - -Uni-Lab 允许通过命令行参数覆盖配置文件中的设置,提供更灵活的配置方式。命令行参数的优先级高于配置文件。 - -### 支持命令行覆盖的配置项 - -以下配置项可以通过命令行参数进行覆盖: - -| 配置类 | 配置字段 | 命令行参数 | 说明 | -| ------------- | ----------------- | ------------------- | -------------------------------- | -| `BasicConfig` | `ak` | `--ak` | 实验室访问密钥 | -| `BasicConfig` | `sk` | `--sk` | 实验室私钥 | -| `BasicConfig` | `working_dir` | `--working_dir` | 工作目录路径 | -| `BasicConfig` | `is_host_mode` | `--is_slave` | 主站模式(参数为从站模式,取反) | -| `BasicConfig` | `slave_no_host` | `--slave_no_host` | 从站模式下跳过等待主机服务 | -| `BasicConfig` | `upload_registry` | `--upload_registry` | 启动时上传注册表信息 | -| `BasicConfig` | `vis_2d_enable` | `--2d_vis` | 启用 2D 可视化 | -| `HTTPConfig` | `remote_addr` | `--addr` | 远程服务地址 | - -### 特殊命令行参数 - -除了直接覆盖配置项的参数外,还有一些特殊的命令行参数: - -| 参数 | 说明 | -| ------------------- | ------------------------------------ | -| `--config` | 指定配置文件路径 | -| `--port` | Web 服务端口(不影响配置文件) | -| `--disable_browser` | 禁用自动打开浏览器(不影响配置文件) | -| `--visual` | 可视化工具选择(不影响配置文件) | -| `--skip_env_check` | 跳过环境检查(不影响配置文件) | - -### 配置优先级 - -配置项的生效优先级从高到低为: - -1. **命令行参数**:最高优先级 -2. **环境变量**:中等优先级 -3. **配置文件**:基础优先级 - -### 使用示例 - -```bash -# 通过命令行覆盖认证信息 -unilab --ak "new_access_key" --sk "new_secret_key" - -# 覆盖服务器地址 -unilab --addr "https://custom.server.com/api/v1" - -# 启用从站模式并跳过等待主机 -unilab --is_slave --slave_no_host - -# 启用上传注册表和2D可视化 -unilab --upload_registry --2d_vis - -# 组合使用多个覆盖参数 -unilab --ak "key" --sk "secret" --addr "test" --upload_registry --2d_vis -``` - -### 预设环境地址 - -`--addr` 参数支持以下预设值,会自动转换为对应的完整 URL: - -- `test` → `https://uni-lab.test.bohrium.com/api/v1` -- `uat` → `https://uni-lab.uat.bohrium.com/api/v1` -- `local` → `http://127.0.0.1:48197/api/v1` -- 其他值 → 直接使用作为完整 URL - -## 配置选项详解 - -### 基础配置 (BasicConfig) - -基础配置包含了系统运行的核心参数: - -| 参数 | 类型 | 默认值 | 说明 | -| ------------------------ | ---- | ------------- | ------------------------------------------ | -| `ak` | str | `""` | 实验室访问密钥(必需) | -| `sk` | str | `""` | 实验室私钥(必需) | -| `working_dir` | str | `""` | 工作目录,通常自动设置 | -| `is_host_mode` | bool | `True` | 是否为主站模式 | -| `slave_no_host` | bool | `False` | 从站模式下是否跳过等待主机服务 | -| `upload_registry` | bool | `False` | 启动时是否上传注册表信息 | -| `machine_name` | str | `"undefined"` | 机器名称,自动从 hostname 获取(不可配置) | -| `vis_2d_enable` | bool | `False` | 是否启用 2D 可视化 | -| `communication_protocol` | str | `"websocket"` | 通信协议,固定为 websocket | - -#### 认证配置 - -`ak` 和 `sk` 是必需的认证参数: - -1. **获取方式**:在 [Uni-Lab 官网](https://uni-lab.bohrium.com) 注册实验室后获得 -2. **配置方式**: - - **命令行参数**:`--ak "your_key" --sk "your_secret"`(最高优先级) - - **配置文件**:在 `BasicConfig` 类中设置 - - **环境变量**:`UNILABOS_BASICCONFIG_AK` 和 `UNILABOS_BASICCONFIG_SK` -3. **优先级顺序**:命令行参数 > 环境变量 > 配置文件 -4. **安全注意**:请妥善保管您的密钥信息 - -**推荐做法**: - -- 开发环境:使用配置文件 -- 生产环境:使用环境变量或命令行参数 -- 临时测试:使用命令行参数 - -### WebSocket 配置 (WSConfig) - -WebSocket 是 Uni-Lab 的主要通信方式: - -| 参数 | 类型 | 默认值 | 说明 | -| ------------------------ | ---- | ------ | ------------------ | -| `reconnect_interval` | int | `5` | 断线重连间隔(秒) | -| `max_reconnect_attempts` | int | `999` | 最大重连次数 | -| `ping_interval` | int | `30` | 心跳检测间隔(秒) | - -### HTTP 配置 (HTTPConfig) - -HTTP 客户端配置用于与云端服务通信: - -| 参数 | 类型 | 默认值 | 说明 | -| ------------- | ---- | --------------------------------- | ------------ | -| `remote_addr` | str | `"http://127.0.0.1:48197/api/v1"` | 远程服务地址 | - -**预设环境地址**: - -- 生产环境:`https://uni-lab.bohrium.com/api/v1` -- 测试环境:`https://uni-lab.test.bohrium.com/api/v1` -- UAT 环境:`https://uni-lab.uat.bohrium.com/api/v1` -- 本地环境:`http://127.0.0.1:48197/api/v1` - -### ROS 配置 (ROSConfig) - -配置 ROS 消息转换器需要加载的模块: - -```python -class ROSConfig: - modules = [ - "std_msgs.msg", # 标准消息类型 - "geometry_msgs.msg", # 几何消息类型 - "control_msgs.msg", # 控制消息类型 - "control_msgs.action", # 控制动作类型 - "nav2_msgs.action", # 导航动作类型 - "unilabos_msgs.msg", # UniLab 自定义消息类型 - "unilabos_msgs.action", # UniLab 自定义动作类型 - ] -``` - -您可以根据实际使用的设备和功能添加其他 ROS 模块。 - -### OSS 上传配置 (OSSUploadConfig) - -对象存储服务配置,用于文件上传功能: - -| 参数 | 类型 | 默认值 | 说明 | -| ------------------- | ---- | ------ | -------------------- | -| `api_host` | str | `""` | OSS API 主机地址 | -| `authorization` | str | `""` | 授权认证信息 | -| `init_endpoint` | str | `""` | 上传初始化端点 | -| `complete_endpoint` | str | `""` | 上传完成端点 | -| `max_retries` | int | `3` | 上传失败最大重试次数 | - -## 环境变量支持 - -Uni-Lab 支持通过环境变量覆盖配置文件中的设置。环境变量格式为: - -``` -UNILABOS_{配置类名}_{字段名} -``` - -### 环境变量示例 - -```bash -# 设置基础配置 -export UNILABOS_BASICCONFIG_AK="your_access_key" -export UNILABOS_BASICCONFIG_SK="your_secret_key" -export UNILABOS_BASICCONFIG_IS_HOST_MODE="true" - -# 设置WebSocket配置 -export UNILABOS_WSCONFIG_RECONNECT_INTERVAL="10" -export UNILABOS_WSCONFIG_MAX_RECONNECT_ATTEMPTS="500" - -# 设置HTTP配置 -export UNILABOS_HTTPCONFIG_REMOTE_ADDR="https://uni-lab.bohrium.com/api/v1" -``` - -### 环境变量类型转换 - -- **布尔值**:`"true"`, `"1"`, `"yes"` → `True`;其他 → `False` -- **整数**:自动转换为 `int` 类型 -- **浮点数**:自动转换为 `float` 类型 -- **字符串**:保持原值 - -## 配置文件使用方法 - -### 1. 指定配置文件启动 - -```bash -# 使用指定配置文件启动 -unilab --config /path/to/your/config.py -``` - -### 2. 使用默认配置文件 - -如果不指定配置文件,系统会按以下顺序查找: - -1. 环境变量 `UNILABOS_BASICCONFIG_CONFIG_PATH` 指定的路径 -2. 工作目录下的 `local_config.py` -3. 首次使用时会引导创建配置文件 - -### 3. 配置文件验证 - -系统启动时会自动验证配置文件: - -- **语法检查**:确保 Python 语法正确 -- **类型检查**:验证配置项类型是否匹配 -- **必需项检查**:确保 `ak` 和 `sk` 已配置 - -## 最佳实践 - -### 1. 安全配置 - -- 不要将包含密钥的配置文件提交到版本控制系统 -- 使用环境变量或命令行参数在生产环境中配置敏感信息 -- 定期更换访问密钥 -- **推荐配置方式**: - - ```bash - # 生产环境 - 使用环境变量 - export UNILABOS_BASICCONFIG_AK="your_access_key" - export UNILABOS_BASICCONFIG_SK="your_secret_key" - unilab - - # 或使用命令行参数 - unilab --ak "your_access_key" --sk "your_secret_key" - ``` - -### 2. 多环境配置 - -为不同环境创建不同的配置文件并结合命令行参数: - -``` -configs/ -├── local_config.py # 本地开发 -├── test_config.py # 测试环境 -├── prod_config.py # 生产环境 -└── example_config.py # 示例配置 -``` - -**环境切换示例**: - -```bash -# 本地开发环境 -unilab --config configs/local_config.py --addr local - -# 测试环境 -unilab --config configs/test_config.py --addr test --upload_registry - -# 生产环境 -unilab --config configs/prod_config.py --ak "$PROD_AK" --sk "$PROD_SK" -``` - -### 3. 配置管理 - -- 保持配置文件简洁,只包含需要修改的配置项 -- 为配置项添加注释说明其作用 -- 定期检查和更新配置文件 -- **命令行参数优先使用场景**: - - 临时测试不同配置 - - CI/CD 流水线中的动态配置 - - 不同环境间快速切换 - - 敏感信息的安全传递 - -### 4. 灵活配置策略 - -**基础配置文件 + 命令行覆盖**的推荐方式: - -```python -# base_config.py - 基础配置 -class BasicConfig: - # 非敏感配置写在文件中 - is_host_mode = True - upload_registry = False - vis_2d_enable = False - -class WSConfig: - reconnect_interval = 5 - max_reconnect_attempts = 999 - ping_interval = 30 -``` - -```bash -# 启动时通过命令行覆盖关键参数 -unilab --config base_config.py \ - --ak "$AK" \ - --sk "$SK" \ - --addr "test" \ - --upload_registry \ - --2d_vis -``` - -## 故障排除 - -### 1. 配置文件加载失败 - -**错误信息**:`[ENV] 配置文件 xxx 不存在` - -**解决方法**: - -- 确认配置文件路径正确 -- 检查文件权限是否可读 -- 确保配置文件是 `.py` 格式 - -### 2. 语法错误 - -**错误信息**:`[ENV] 加载配置文件 xxx 失败` - -**解决方法**: - -- 检查 Python 语法是否正确 -- 确认类名和字段名拼写正确 -- 验证缩进是否正确(使用空格而非制表符) - -### 3. 认证失败 - -**错误信息**:`后续运行必须拥有一个实验室` - -**解决方法**: - -- 确认 `ak` 和 `sk` 已正确配置 -- 检查密钥是否有效 -- 确认网络连接正常 - -### 4. 环境变量不生效 - -**解决方法**: - -- 确认环境变量名格式正确(`UNILABOS_CLASS_FIELD`) -- 检查环境变量是否已正确设置 -- 重启系统或重新加载环境变量 - -### 5. 命令行参数不生效 - -**错误现象**:设置了命令行参数但配置没有生效 - -**解决方法**: - -- 确认参数名拼写正确(如 `--ak` 而不是 `--access_key`) -- 检查参数格式是否正确(布尔参数如 `--is_slave` 不需要值) -- 确认参数位置正确(所有参数都应在 `unilab` 之后) -- 查看启动日志确认参数是否被正确解析 - -### 6. 配置优先级混淆 - -**错误现象**:不确定哪个配置生效 - -**解决方法**: - -- 记住优先级:命令行参数 > 环境变量 > 配置文件 -- 使用 `--ak` 和 `--sk` 参数时会看到提示信息 -- 检查启动日志中的配置加载信息 -- 临时移除低优先级配置来测试高优先级配置是否生效 diff --git a/docs/user_guide/graph_files.md b/docs/user_guide/graph_files.md new file mode 100644 index 00000000..a8f86aaf --- /dev/null +++ b/docs/user_guide/graph_files.md @@ -0,0 +1,860 @@ +# 设备图文件说明 + +设备图文件定义了实验室中所有设备、资源及其连接关系。本文档说明如何创建和使用设备图文件。 + +## 概述 + +设备图文件采用 JSON 格式,节点定义基于 **`ResourceDict`** 标准模型(定义在 `unilabos.ros.nodes.resource_tracker`)。系统会自动处理旧格式并转换为标准格式,确保向后兼容性。 + +**核心概念**: + +- **Nodes(节点)**: 代表设备或资源,通过 `parent` 字段建立层级关系 +- **Links(连接)**: 可选的连接关系定义,用于展示设备间的物理或通信连接 +- **UUID**: 全局唯一标识符,用于跨系统的资源追踪 +- **自动转换**: 旧格式会通过 `ResourceDictInstance.get_resource_instance_from_dict()` 自动转换 + +## 文件格式 + +Uni-Lab 支持两种格式的设备图文件: + +### JSON 格式(推荐) + +**优点**: + +- 易于编辑和阅读 +- 支持注释(使用预处理) +- 与 Web 界面完全兼容 +- 便于版本控制 + +**示例**: `workshop1.json` + +### GraphML 格式 + +**优点**: + +- 可用图形化工具编辑(如 yEd) +- 适合复杂拓扑可视化 + +**示例**: `setup.graphml` + +## JSON 文件结构 + +一个完整的 JSON 设备图文件包含两个主要部分: + +```json +{ + "nodes": [ + /* 设备和资源节点 */ + ], + "links": [ + /* 连接关系(可选)*/ + ] +} +``` + +### Nodes(节点) + +每个节点代表一个设备或资源。节点的定义遵循 `ResourceDict` 标准模型: + +```json +{ + "id": "liquid_handler_1", + "uuid": "550e8400-e29b-41d4-a716-446655440000", + "name": "液体处理工作站", + "type": "device", + "class": "liquid_handler", + "config": { + "port": "/dev/ttyUSB0", + "baudrate": 9600 + }, + "data": {}, + "position": { + "x": 100, + "y": 200 + }, + "parent": null +} +``` + +**字段说明(基于 ResourceDict 标准定义)**: + +| 字段 | 必需 | 说明 | 示例 | 默认值 | +| ------------- | ---- | ------------------------ | ---------------------------------------------------- | -------- | +| `id` | ✓ | 唯一标识符 | `"pump_1"` | - | +| `uuid` | | 全局唯一标识符 (UUID) | `"550e8400-e29b-41d4-a716-446655440000"` | 自动生成 | +| `name` | ✓ | 显示名称 | `"主反应泵"` | - | +| `type` | ✓ | 节点类型 | `"device"`, `"resource"`, `"container"`, `"deck"` 等 | - | +| `class` | ✓ | 设备/资源类别 | `"liquid_handler"`, `"syringepump.runze"` | `""` | +| `config` | | Python 类的初始化参数 | `{"port": "COM3"}` | `{}` | +| `data` | | 资源的运行状态数据 | `{"status": "Idle", "position": 0.0}` | `{}` | +| `position` | | 在图中的位置 | `{"x": 100, "y": 200}` 或完整的 pose 结构 | - | +| `pose` | | 完整的 3D 位置信息 | 参见下文 | - | +| `parent` | | 父节点 ID | `"deck_1"` | `null` | +| `parent_uuid` | | 父节点 UUID | `"550e8400-..."` | `null` | +| `children` | | 子节点 ID 列表(旧格式) | `["child1", "child2"]` | - | +| `description` | | 资源描述 | `"用于精确控制试剂A的加料速率"` | `""` | +| `schema` | | 资源 schema 定义 | `{}` | `{}` | +| `model` | | 资源 3D 模型信息 | `{}` | `{}` | +| `icon` | | 资源图标 | `"pump.webp"` | `""` | +| `extra` | | 额外的自定义数据 | `{"custom_field": "value"}` | `{}` | + +### Position 和 Pose(位置信息) + +**简单格式(旧格式,兼容)**: + +```json +"position": { + "x": 100, + "y": 200, + "z": 0 +} +``` + +**完整格式(推荐)**: + +```json +"pose": { + "size": { + "width": 127.76, + "height": 85.48, + "depth": 10.0 + }, + "scale": { + "x": 1.0, + "y": 1.0, + "z": 1.0 + }, + "layout": "x-y", + "position": { + "x": 100, + "y": 200, + "z": 0 + }, + "position3d": { + "x": 100, + "y": 200, + "z": 0 + }, + "rotation": { + "x": 0, + "y": 0, + "z": 0 + }, + "cross_section_type": "rectangle" +} +``` + +### Links(连接) + +定义节点之间的连接关系(可选,主要用于物理连接或通信关系的可视化): + +```json +{ + "source": "pump_1", + "target": "reactor_1", + "sourceHandle": "output", + "targetHandle": "input", + "type": "physical" +} +``` + +**字段说明**: + +| 字段 | 必需 | 说明 | 示例 | +| -------------- | ---- | ---------------- | ---------------------------------------- | +| `source` | ✓ | 源节点 ID | `"pump_1"` | +| `target` | ✓ | 目标节点 ID | `"reactor_1"` | +| `sourceHandle` | | 源节点的连接点 | `"output"` | +| `targetHandle` | | 目标节点的连接点 | `"input"` | +| `type` | | 连接类型 | `"physical"`, `"communication"` | +| `port` | | 端口映射信息 | `{"source": "port1", "target": "port2"}` | + +**注意**: Links 主要用于图形化展示和文档说明,父子关系通过 `parent` 字段定义,不依赖 links。 + +## 完整示例 + +### 示例 1:液体处理工作站(PRCXI9300) + +这是一个真实的液体处理工作站配置,包含设备、工作台和多个板资源。 + +**文件位置**: `test/experiments/prcxi_9300.json` + +```json +{ + "nodes": [ + { + "id": "PRCXI9300", + "name": "PRCXI9300", + "parent": null, + "type": "device", + "class": "liquid_handler.prcxi", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "deck": { + "_resource_child_name": "PRCXI_Deck_9300", + "_resource_type": "unilabos.devices.liquid_handling.prcxi.prcxi:PRCXI9300Deck" + }, + "host": "10.181.214.132", + "port": 9999, + "timeout": 10.0, + "axis": "Left", + "channel_num": 8, + "setup": false, + "debug": true, + "simulator": true, + "matrix_id": "71593" + }, + "data": {}, + "children": ["PRCXI_Deck_9300"] + }, + { + "id": "PRCXI_Deck_9300", + "name": "PRCXI_Deck_9300", + "parent": "PRCXI9300", + "type": "deck", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "PRCXI9300Deck", + "size_x": 100, + "size_y": 100, + "size_z": 100, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "deck" + }, + "data": {}, + "children": [ + "RackT1", + "PlateT2", + "trash", + "PlateT4", + "PlateT5", + "PlateT6" + ] + }, + { + "id": "RackT1", + "name": "RackT1", + "parent": "PRCXI_Deck_9300", + "type": "tip_rack", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "TipRack", + "size_x": 127.76, + "size_y": 85.48, + "size_z": 100 + }, + "data": {}, + "children": [] + } + ] +} +``` + +**关键点**: + +- 使用 `parent` 字段建立层级关系(PRCXI9300 → Deck → Rack/Plate) +- 使用 `children` 字段(旧格式)列出子节点 +- `config` 中包含设备特定的连接参数 +- `data` 存储运行时状态 +- `position` 使用简单的 x/y/z 坐标 + +### 示例 2:有机合成工作站(带 Links) + +这是一个格林纳德反应的流动化学工作站配置,展示了完整的设备连接和通信关系。 + +**文件位置**: `test/experiments/Grignard_flow_batchreact_single_pumpvalve.json` + +```json +{ + "nodes": [ + { + "id": "YugongStation", + "name": "愚公常量合成工作站", + "parent": null, + "type": "device", + "class": "workstation", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "protocol_type": [ + "PumpTransferProtocol", + "CleanProtocol", + "SeparateProtocol", + "EvaporateProtocol" + ] + }, + "data": {}, + "children": [ + "serial_pump", + "pump_reagents", + "flask_CH2Cl2", + "reactor", + "pump_workup", + "separator_controller", + "flask_separator", + "rotavap", + "column" + ] + }, + { + "id": "serial_pump", + "name": "serial_pump", + "parent": "YugongStation", + "type": "device", + "class": "serial", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "COM7", + "baudrate": 9600 + }, + "data": {}, + "children": [] + }, + { + "id": "pump_reagents", + "name": "pump_reagents", + "parent": "YugongStation", + "type": "device", + "class": "syringepump.runze", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "port": "/devices/PumpBackbone/Serial/serialwrite", + "address": "1", + "max_volume": 25.0 + }, + "data": { + "max_velocity": 1.0, + "position": 0.0, + "status": "Idle", + "valve_position": "0" + }, + "children": [] + }, + { + "id": "reactor", + "name": "reactor", + "parent": "YugongStation", + "type": "container", + "class": null, + "position": { + "x": 430.4087301587302, + "y": 428, + "z": 0 + }, + "config": {}, + "data": {}, + "children": [] + } + ], + "links": [ + { + "source": "pump_reagents", + "target": "serial_pump", + "type": "communication", + "port": { + "pump_reagents": "port", + "serial_pump": "port" + } + }, + { + "source": "pump_workup", + "target": "serial_pump", + "type": "communication", + "port": { + "pump_workup": "port", + "serial_pump": "port" + } + } + ] +} +``` + +**关键点**: + +- 多级设备层次:工作站包含多个子设备和容器 +- `links` 定义通信关系(泵通过串口连接) +- `data` 字段存储设备状态(如泵的位置、速度等) +- `class` 可以使用点号分层(如 `"syringepump.runze"`) +- 容器的 `class` 可以为 `null` + +## 格式兼容性和转换 + +### 旧格式自动转换 + +Uni-Lab 使用 `ResourceDictInstance.get_resource_instance_from_dict()` 方法自动处理旧格式的节点数据,确保向后兼容性。 + +**自动转换规则**: + +1. **自动生成缺失字段**: + + ```python + # 如果缺少 id,使用 name 作为 id + if "id" not in content: + content["id"] = content["name"] + + # 如果缺少 uuid,自动生成 + if "uuid" not in content: + content["uuid"] = str(uuid.uuid4()) + ``` + +2. **Position 格式转换**: + + ```python + # 旧格式:简单的 x/y 坐标 + "position": {"x": 100, "y": 200} + + # 自动转换为新格式 + "position": { + "position": {"x": 100, "y": 200} + } + ``` + +3. **默认值填充**: + + ```python + # 自动填充空字段 + if not content.get("class"): + content["class"] = "" + if not content.get("config"): + content["config"] = {} + if not content.get("data"): + content["data"] = {} + if not content.get("extra"): + content["extra"] = {} + ``` + +4. **Pose 字段同步**: + ```python + # 如果没有 pose,使用 position + if "pose" not in content: + content["pose"] = content.get("position", {}) + ``` + +### 使用示例 + +```python +from unilabos.ros.nodes.resource_tracker import ResourceDictInstance + +# 旧格式节点 +old_format_node = { + "name": "pump_1", + "type": "device", + "class": "syringepump", + "position": {"x": 100, "y": 200} +} + +# 自动转换为标准格式 +instance = ResourceDictInstance.get_resource_instance_from_dict(old_format_node) + +# 访问标准化后的数据 +print(instance.res_content.id) # "pump_1" +print(instance.res_content.uuid) # 自动生成的 UUID +print(instance.res_content.config) # {} +print(instance.res_content.data) # {} +``` + +### 格式迁移建议 + +虽然系统会自动处理旧格式,但建议在新文件中使用完整的标准格式: + +| 字段 | 旧格式(兼容) | 新格式(推荐) | +| ------ | ---------------------------------- | ------------------------------------------------ | +| 标识符 | 仅 `id` 或仅 `name` | `id` + `uuid` | +| 位置 | `"position": {"x": 100, "y": 200}` | 完整的 `pose` 结构 | +| 父节点 | `"parent": "parent_id"` | `"parent": "parent_id"` + `"parent_uuid": "..."` | +| 配置 | 可省略 | 显式设置为 `{}` | +| 数据 | 可省略 | 显式设置为 `{}` | + +## 节点类型详解 + +### Device 节点 + +设备节点代表实际的硬件设备: + +```json +{ + "id": "device_id", + "name": "设备名称", + "type": "device", + "class": "设备类别", + "parent": null, + "config": { + "port": "COM3" + }, + "data": {}, + "children": [] +} +``` + +**常见设备类别**: + +- `liquid_handler`: 液体处理工作站 +- `liquid_handler.prcxi`: PRCXI 液体处理工作站 +- `syringepump`: 注射泵 +- `syringepump.runze`: 润泽注射泵 +- `heaterstirrer`: 加热搅拌器 +- `balance`: 天平 +- `reactor_vessel`: 反应釜 +- `serial`: 串口通信设备 +- `workstation`: 自动化工作站 + +### Resource 节点 + +资源节点代表物料容器、载具等: + +```json +{ + "id": "resource_id", + "name": "资源名称", + "type": "resource", + "class": "资源类别", + "parent": "父节点ID", + "config": { + "size_x": 127.76, + "size_y": 85.48, + "size_z": 100 + }, + "data": {}, + "children": [] +} +``` + +**常见资源类型**: + +- `deck`: 工作台/甲板 +- `plate`: 板(96 孔板等) +- `tip_rack`: 枪头架 +- `tube`: 试管 +- `container`: 容器 +- `well`: 孔位 +- `bottle_carrier`: 瓶架 + +## Handle(连接点) + +每个设备和资源可以有多个连接点(handles),用于定义可以连接的接口。 + +### 查看可用 handles + +设备和资源的可用 handles 定义在注册表中: + +```yaml +# 设备注册表示例 +liquid_handler: + handles: + - handler_key: pipette + io_type: source + - handler_key: deck + io_type: target +``` + +### 常见 handles + +| 设备类型 | Source Handles | Target Handles | +| ---------- | -------------- | -------------- | +| 泵 | output | input | +| 反应釜 | output, vessel | input | +| 液体处理器 | pipette | deck | +| 板 | wells | access | + +## 使用 Web 界面创建图文件 + +Uni-Lab 提供 Web 界面来可视化创建和编辑设备图: + +### 1. 启动 Uni-Lab + +```bash +unilab +``` + +### 2. 访问 Web 界面 + +打开浏览器访问 `http://localhost:8002` + +### 3. 图形化编辑 + +- 拖拽添加设备和资源 +- 连线建立连接关系 +- 编辑节点属性 +- 保存为 JSON 文件 + +### 4. 导出图文件 + +点击"导出"按钮,下载 JSON 文件到本地。 + +## 从云端获取图文件 + +如果不指定`-g`参数,Uni-Lab 会自动从云端获取: + +```bash +# 使用云端配置 +unilab + +# 日志会显示: +# [INFO] 未指定设备加载文件路径,尝试从HTTP获取... +# [INFO] 联网获取设备加载文件成功 +``` + +**云端图文件管理**: + +1. 登录 https://uni-lab.bohrium.com +2. 进入"设备配置" +3. 创建或编辑配置 +4. 保存到云端 + +本地启动时会自动同步最新配置。 + +## 调试图文件 + +### 验证 JSON 格式 + +```bash +# 使用Python验证 +python -c "import json; json.load(open('workshop1.json'))" + +# 使用在线工具 +# https://jsonlint.com/ +``` + +### 检查节点引用 + +确保: + +- 所有`links`中的`source`和`target`都存在于`nodes`中 +- `parent`字段指向的节点存在 +- `class`字段对应的设备/资源在注册表中存在 + +### 启动时验证 + +```bash +# Uni-Lab启动时会验证图文件 +unilab -g workshop1.json + +# 查看日志中的错误或警告 +# [ERROR] 节点 xxx 的source端点 yyy 不存在 +# [WARNING] 节点 zzz missing 'name', defaulting to ... +``` + +## 最佳实践 + +### 1. 命名规范 + +```json +{ + "id": "pump_reagent_1", // 小写+下划线,描述性 + "name": "试剂进料泵A", // 中文显示名称 + "class": "syringepump" // 使用注册表中的精确名称 +} +``` + +### 2. 层级组织 + +``` +host_node (主节点) +└── liquid_handler_1 (设备) + └── deck_1 (资源) + ├── tiprack_1 (资源) + ├── plate_1 (资源) + └── reservoir_1 (资源) +``` + +### 3. 配置分离 + +将设备特定配置放在`config`中: + +```json +{ + "id": "pump_1", + "class": "syringepump", + "config": { + "port": "COM3", // 设备特定 + "max_flow_rate": 10, // 设备特定 + "volume": 50 // 设备特定 + } +} +``` + +### 4. 版本控制 + +```bash +# 使用Git管理图文件 +git add workshop1.json +git commit -m "Add new liquid handler configuration" + +# 使用有意义的文件名 +workshop_v1.json +workshop_production.json +workshop_test.json +``` + +### 5. 注释(通过描述字段) + +虽然 JSON 不支持注释,但可以使用`description`字段: + +```json +{ + "id": "pump_1", + "name": "进料泵", + "description": "用于精确控制试剂A的加料速率,最大流速10mL/min", + "class": "syringepump" +} +``` + +## 示例文件位置 + +Uni-Lab 在安装时已预置了 **40+ 个真实的设备图文件示例**,位于 `unilabos/test/experiments/` 目录。这些都是真实项目中使用的配置文件,可以直接使用或作为参考。 + +### 📁 主要示例文件 + +``` +test/experiments/ +├── workshop.json # 综合工作台(推荐新手) +├── empty_devices.json # 空设备配置(最小化) +├── prcxi_9300.json # PRCXI液体处理工作站(本文示例1) +├── prcxi_9320.json # PRCXI 9320工作站 +├── biomek.json # Biomek液体处理工作站 +├── Grignard_flow_batchreact_single_pumpvalve.json # 格林纳德反应工作站(本文示例2) +├── dispensing_station_bioyond.json # Bioyond配液站 +├── reaction_station_bioyond.json # Bioyond反应站 +├── HPLC.json # HPLC分析系统 +├── plr_test.json # PyLabRobot测试配置 +├── lidocaine-graph.json # 利多卡因合成工作站 +├── opcua_example.json # OPC UA设备集成示例 +│ +├── mock_devices/ # 虚拟设备(用于离线测试) +│ ├── mock_all.json # 完整虚拟设备集 +│ ├── mock_pump.json # 虚拟泵 +│ ├── mock_stirrer.json # 虚拟搅拌器 +│ ├── mock_heater.json # 虚拟加热器 +│ └── ... # 更多虚拟设备 +│ +├── Protocol_Test_Station/ # 协议测试工作站 +│ ├── pumptransfer_test_station.json # 泵转移协议测试 +│ ├── heatchill_protocol_test_station.json # 加热冷却协议测试 +│ ├── filter_protocol_test_station.json # 过滤协议测试 +│ └── ... # 更多协议测试 +│ +└── comprehensive_protocol/ # 综合协议示例 + ├── comprehensive_station.json # 综合工作站 + └── comprehensive_slim.json # 精简版综合工作站 +``` + +### 🚀 快速使用 + +无需下载或创建,直接使用 `-g` 参数指定路径: + +```bash +# 使用简单工作台(推荐新手) +unilab --ak your_ak --sk your_sk -g test/experiments/workshop.json + +# 使用虚拟设备(无需真实硬件) +unilab --ak your_ak --sk your_sk -g test/experiments/mock_devices/mock_all.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 +``` + +### 📚 文件分类 + +| 类别 | 说明 | 文件数量 | +| ------------ | ------------------------ | -------- | +| **主工作站** | 完整的实验工作站配置 | 15+ | +| **虚拟设备** | 用于开发测试的 mock 设备 | 10+ | +| **协议测试** | 各种实验协议的测试配置 | 12+ | +| **综合示例** | 包含多种协议的综合工作站 | 3+ | + +这些文件展示了不同场景下的设备图配置,涵盖液体处理、有机合成、分析检测等多个领域,是学习和创建自己配置的绝佳参考。 + +## 快速参考:ResourceDict 完整字段列表 + +基于 `unilabos.ros.nodes.resource_tracker.ResourceDict` 的完整字段定义: + +```python +class ResourceDict(BaseModel): + # === 基础标识 === + id: str # 资源ID(必需) + uuid: str # 全局唯一标识符(自动生成) + name: str # 显示名称(必需) + + # === 类型和分类 === + type: Union[Literal["device"], str] # 节点类型(必需) + klass: str # 资源类别(alias="class",必需) + + # === 层级关系 === + parent: Optional[ResourceDict] # 父资源对象(不序列化) + parent_uuid: Optional[str] # 父资源UUID + + # === 位置和姿态 === + position: ResourceDictPosition # 位置信息 + pose: ResourceDictPosition # 姿态信息(推荐使用) + + # === 配置和数据 === + config: Dict[str, Any] # 设备配置参数 + data: Dict[str, Any] # 运行时状态数据 + extra: Dict[str, Any] # 额外自定义数据 + + # === 元数据 === + description: str # 资源描述 + resource_schema: Dict[str, Any] # schema定义(alias="schema") + model: Dict[str, Any] # 3D模型信息 + icon: str # 图标路径 +``` + +**Position/Pose 结构**: + +```python +class ResourceDictPosition(BaseModel): + size: ResourceDictPositionSize # width, height, depth + scale: ResourceDictPositionScale # x, y, z + layout: Literal["2d", "x-y", "z-y", "x-z"] + position: ResourceDictPositionObject # x, y, z + position3d: ResourceDictPositionObject # x, y, z + rotation: ResourceDictPositionObject # x, y, z + cross_section_type: Literal["rectangle", "circle", "rounded_rectangle"] +``` + +## 下一步 + +- {doc}`../boot_examples/index` - 查看完整启动示例 +- {doc}`../developer_guide/add_device` - 了解如何添加新设备 +- {doc}`06_troubleshooting` - 图文件相关问题排查 +- 源码参考: `unilabos/ros/nodes/resource_tracker.py` - ResourceDict 标准定义 + +## 获取帮助 + +- 在 Web 界面中使用模板创建 +- 参考示例文件:`test/experiments/` 目录 +- 查看 ResourceDict 源码了解完整定义 +- [GitHub 讨论区](https://github.com/dptech-corp/Uni-Lab-OS/discussions) diff --git a/docs/user_guide/image/test_latency_result.png b/docs/user_guide/image/test_latency_result.png new file mode 100644 index 00000000..af6395d9 Binary files /dev/null and b/docs/user_guide/image/test_latency_result.png differ diff --git a/docs/user_guide/image/test_latency_running.png b/docs/user_guide/image/test_latency_running.png new file mode 100644 index 00000000..4483dc13 Binary files /dev/null and b/docs/user_guide/image/test_latency_running.png differ diff --git a/docs/user_guide/image/test_latency_select_device.png b/docs/user_guide/image/test_latency_select_device.png new file mode 100644 index 00000000..af6395d9 Binary files /dev/null and b/docs/user_guide/image/test_latency_select_device.png differ diff --git a/docs/user_guide/installation.md b/docs/user_guide/installation.md index 63a1c1b3..6513150d 100644 --- a/docs/user_guide/installation.md +++ b/docs/user_guide/installation.md @@ -1,43 +1,555 @@ -# **Uni-Lab 安装** +# Uni-Lab-OS 安装指南 -## 快速开始 +本指南提供 Uni-Lab-OS 的完整安装说明,涵盖从快速一键安装到完整开发环境配置的所有方式。 -1. **配置 Conda 环境** +## 系统要求 -Uni-Lab-OS 建议使用 `mamba` 管理环境。创建新的环境: +- **操作系统**: Windows 10/11, Linux (Ubuntu 20.04+), macOS (10.15+) +- **内存**: 最小 4GB,推荐 8GB 以上 +- **磁盘空间**: 至少 10GB 可用空间 +- **网络**: 稳定的互联网连接(用于下载软件包) +- **其他**: + - 已安装 Conda/Miniconda/Miniforge/Mamba + - 开发者需要 Git 和基本的 Python 开发知识 + - 自定义 msgs 需要 GitHub 账号 -```shell +## 安装方式选择 + +根据您的使用场景,选择合适的安装方式: + +| 安装方式 | 适用人群 | 特点 | 安装时间 | +| ---------------------- | -------------------- | ------------------------------ | ---------------------------- | +| **方式一:一键安装** | 实验室用户、快速体验 | 预打包环境,离线可用,无需配置 | 5-10 分钟 (网络良好的情况下) | +| **方式二:手动安装** | 标准用户、生产环境 | 灵活配置,版本可控 | 10-20 分钟 | +| **方式三:开发者安装** | 开发者、需要修改源码 | 可编辑模式,支持自定义 msgs | 20-30 分钟 | + +--- + +## 方式一:一键安装(推荐新用户) + +使用预打包的 conda 环境,最快速的安装方法。 + +### 前置条件 + +确保已安装 Conda/Miniconda/Miniforge/Mamba。 + +### 安装步骤 + +#### 第一步:下载预打包环境 + +1. 访问 [GitHub Actions - Conda Pack Build](https://github.com/dptech-corp/Uni-Lab-OS/actions/workflows/conda-pack-build.yml) + +2. 选择最新的成功构建记录(绿色勾号 ✓) + +3. 在页面底部的 "Artifacts" 部分,下载对应你操作系统的压缩包: + - Windows: `unilab-pack-win-64-{branch}.zip` + - macOS (Intel): `unilab-pack-osx-64-{branch}.tar.gz` + - macOS (Apple Silicon): `unilab-pack-osx-arm64-{branch}.tar.gz` + - Linux: `unilab-pack-linux-64-{branch}.tar.gz` + +#### 第二步:解压并运行安装脚本 + +**Windows**: + +```batch +REM 使用 Windows 资源管理器解压下载的 zip 文件 +REM 或使用命令行: +tar -xzf unilab-pack-win-64-dev.zip + +REM 进入解压后的目录 +cd unilab-pack-win-64-dev + +REM 双击运行 install_unilab.bat +REM 或在命令行中执行: +install_unilab.bat +``` + +**macOS**: + +```bash +# 解压下载的压缩包 +tar -xzf unilab-pack-osx-arm64-dev.tar.gz + +# 进入解压后的目录 +cd unilab-pack-osx-arm64-dev + +# 运行安装脚本 +bash install_unilab.sh +``` + +**Linux**: + +```bash +# 解压下载的压缩包 +tar -xzf unilab-pack-linux-64-dev.tar.gz + +# 进入解压后的目录 +cd unilab-pack-linux-64-dev + +# 添加执行权限(如果需要) +chmod +x install_unilab.sh + +# 运行安装脚本 +./install_unilab.sh +``` + +#### 第三步:激活环境 + +```bash +conda activate unilab +``` + +激活后,您的命令行提示符应该会显示 `(unilab)` 前缀。 + +--- + +## 方式二:手动安装(标准用户) + +适合生产环境和需要灵活配置的用户。 + +### 第一步:安装 Mamba 环境管理器 + +Mamba 是 Conda 的快速替代品,我们强烈推荐使用 Mamba 来管理 Uni-Lab 环境。 + +#### Windows + +下载并安装 Miniforge(包含 Mamba): + +```powershell +# 访问 https://github.com/conda-forge/miniforge/releases +# 下载 Miniforge3-Windows-x86_64.exe +# 运行安装程序 + +# 也可以使用镜像站 https://mirrors.tuna.tsinghua.edu.cn/github-release/conda-forge/miniforge/LatestRelease/ +# 下载 Miniforge3-Windows-x86_64.exe +# 运行安装程序 +``` + +#### Linux/macOS + +```bash +# 下载 Miniforge 安装脚本 +curl -L -O "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh" + +# 运行安装 +bash Miniforge3-$(uname)-$(uname -m).sh + +# 按照提示完成安装,建议选择 yes 来初始化 +``` + +安装完成后,重新打开终端使 Mamba 生效。 + +### 第二步:创建 Uni-Lab 环境 + +使用以下命令创建 Uni-Lab 专用环境: + +```bash +mamba create -n unilab python=3.11.11 # 目前ros2组件依赖版本大多为3.11.11 +mamba activate unilab +mamba install -n unilab uni-lab::unilabos -c robostack-staging -c conda-forge +``` + +**参数说明**: + +- `-n unilab`: 创建名为 "unilab" 的环境 +- `uni-lab::unilabos`: 从 uni-lab channel 安装 unilabos 包 +- `-c robostack-staging -c conda-forge`: 添加额外的软件源 + +**如果遇到网络问题**,可以使用清华镜像源加速下载: + +```bash +# 配置清华镜像源 +mamba config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/ +mamba config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/ +mamba config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/ + +# 然后重新执行安装命令 +mamba create -n unilab uni-lab::unilabos -c robostack-staging +``` + +### 第三步:激活环境 + +```bash +conda activate unilab +``` + +--- + +## 方式三:开发者安装 + +适用于需要修改 Uni-Lab 源代码或开发新设备驱动的开发者。 + +### 前置条件 + +- 已安装 Git +- 已安装 Mamba/Conda +- 有 GitHub 账号(如需自定义 msgs) +- 基本的 Python 开发知识 + +### 第一步:克隆仓库 + +```bash +git clone https://github.com/dptech-corp/Uni-Lab-OS.git +cd Uni-Lab-OS +``` + +如果您需要贡献代码,建议先 Fork 仓库: + +1. 访问 https://github.com/dptech-corp/Uni-Lab-OS +2. 点击右上角的 "Fork" 按钮 +3. Clone 您的 Fork 版本: + ```bash + git clone https://github.com/YOUR_USERNAME/Uni-Lab-OS.git + cd Uni-Lab-OS + ``` + +### 第二步:安装基础环境 + +**推荐方式**:先通过**方式一(一键安装)**或**方式二(手动安装)**完成基础环境的安装,这将包含所有必需的依赖项(ROS2、msgs 等)。 + +#### 选项 A:通过一键安装(推荐) + +参考上文"方式一:一键安装",完成基础环境的安装后,激活环境: + +```bash +conda activate unilab +``` + +#### 选项 B:通过手动安装 + +参考上文"方式二:手动安装",创建并安装环境: + +```bash +mamba create -n unilab python=3.11.11 +conda activate unilab +mamba install -n unilab uni-lab::unilabos -c robostack-staging -c conda-forge +``` + +**说明**:这会安装包括 Python 3.11.11、ROS2 Humble、ros-humble-unilabos-msgs 和所有必需依赖 + +### 第三步:切换到开发版本 + +现在你已经有了一个完整可用的 Uni-Lab 环境,接下来将 unilabos 包切换为开发版本: + +```bash +# 确保环境已激活 +conda activate unilab + +# 卸载 pip 安装的 unilabos(保留所有 conda 依赖) +pip uninstall unilabos -y + +# 克隆 dev 分支(如果还未克隆) +cd /path/to/your/workspace +git clone -b dev https://github.com/dptech-corp/Uni-Lab-OS.git +# 或者如果已经克隆,切换到 dev 分支 +cd Uni-Lab-OS +git checkout dev +git pull + +# 以可编辑模式安装开发版 unilabos +pip install -e . -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple +``` + +**参数说明**: + +- `-e`: editable mode(可编辑模式),代码修改立即生效,无需重新安装 +- `-i`: 使用清华镜像源加速下载 +- `pip uninstall unilabos`: 只卸载 pip 安装的 unilabos 包,不影响 conda 安装的其他依赖(如 ROS2、msgs 等) + +### 第四步:安装或自定义 ros-humble-unilabos-msgs(可选) + +Uni-Lab 使用 ROS2 消息系统进行设备间通信。如果你使用方式一或方式二安装,msgs 包已经自动安装。 + +#### 使用已安装的 msgs(大多数用户) + +如果你不需要修改 msgs,可以跳过此步骤,直接使用已安装的 msgs 包。验证安装: + +```bash +# 列出所有 unilabos_msgs 接口 +ros2 interface list | grep unilabos_msgs + +# 查看特定 action 定义 +ros2 interface show unilabos_msgs/action/DeviceCmd +``` + +#### 自定义 msgs(高级用户) + +如果你需要: + +- 添加新的 ROS2 action 定义 +- 修改现有 msg/srv/action 接口 +- 为特定设备定制通信协议 + +请参考 **[添加新动作指令(Action)指南](../developer_guide/add_action.md)**,该指南详细介绍了如何: + +- 编写新的 Action 定义 +- 在线构建 Action(通过 GitHub Actions) +- 下载并安装自定义的 msgs 包 +- 测试和验证新的 Action + +```bash +# 安装自定义构建的 msgs 包 +mamba remove --force ros-humble-unilabos-msgs +mamba config set safety_checks disabled # 关闭 md5 检查 +mamba install /path/to/ros-humble-unilabos-msgs-*.conda --offline +``` + +### 第五步:验证开发环境 + +完成上述步骤后,验证开发环境是否正确配置: + +```bash +# 确保环境已激活 +conda activate unilab + +# 检查 ROS2 环境 +ros2 --version + +# 检查 msgs 包 +ros2 interface list | grep unilabos_msgs + +# 检查 Python 可以导入 unilabos +python -c "import unilabos; print(f'Uni-Lab版本: {unilabos.__version__}')" + +# 检查 unilab 命令 +unilab --help +``` + +如果所有命令都正常输出,说明开发环境配置成功! + +### 开发工具推荐 + +#### IDE + +- **PyCharm Professional**: 强大的 Python IDE,支持远程调试 +- **VS Code**: 轻量级,配合 Python 扩展使用 +- **Vim/Emacs**: 适合终端开发 + +#### 推荐的 VS Code 扩展 + +- Python +- Pylance +- ROS +- URDF +- YAML + +#### 调试工具 + +```bash +# 安装调试工具 +pip install ipdb pytest pytest-cov -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple + +# 代码质量检查 +pip install black flake8 mypy -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple +``` + +### 设置 pre-commit 钩子(可选) + +```bash +# 安装 pre-commit +pip install pre-commit -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple + +# 设置钩子 +pre-commit install + +# 手动运行检查 +pre-commit run --all-files +``` + +--- + +## 验证安装 + +无论使用哪种安装方式,都应该验证安装是否成功。 + +### 基本验证 + +```bash +# 确保已激活环境 +conda activate unilab # 或 unilab-dev + +# 检查 unilab 命令 +unilab --help +``` + +您应该看到类似以下的输出: + +``` +usage: unilab [-h] [-g GRAPH] [-c CONTROLLERS] [--registry_path REGISTRY_PATH] + [--working_dir WORKING_DIR] [--backend {ros,simple,automancer}] + ... +``` + +### 检查版本 + +```bash +python -c "import unilabos; print(f'Uni-Lab版本: {unilabos.__version__}')" +``` + +### 使用验证脚本(方式一) + +如果使用一键安装,可以运行预打包的验证脚本: + +```bash +# 确保已激活环境 +conda activate unilab + +# 运行验证脚本 +python verify_installation.py +``` + +如果看到 "✓ All checks passed!",说明安装成功! + +--- + +## 常见问题 + +### 问题 1: 找不到 unilab 命令 + +**原因**: 环境未正确激活或 PATH 未设置 + +**解决方案**: + +```bash +# 确保激活了正确的环境 +conda activate unilab + +# 检查 unilab 是否在 PATH 中 +which unilab # Linux/macOS +where unilab # Windows +``` + +### 问题 2: 包冲突或依赖错误 + +**解决方案**: + +```bash +# 删除旧环境重新创建 +conda deactivate +conda env remove -n unilab mamba create -n unilab uni-lab::unilabos -c robostack-staging -c conda-forge ``` -2. **安装开发版 Uni-Lab-OS** +### 问题 3: 下载速度慢 -```shell -# 配置好conda环境后,克隆仓库 -git clone https://github.com/dptech-corp/Uni-Lab-OS.git -b dev -cd Uni-Lab-OS +**解决方案**: 使用国内镜像源(清华、中科大等) -# 安装 Uni-Lab-OS -pip install -e . +```bash +# 查看当前 channel 配置 +conda config --show channels + +# 添加清华镜像 +conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/ ``` -3. **安装开发版 ros-humble-unilabos-msgs** +### 问题 4: 权限错误 -**卸载老版本:** -```shell +**Windows 解决方案**: 以管理员身份运行命令提示符 + +**Linux/macOS 解决方案**: + +```bash +# 不要使用 sudo 安装 conda 包 +# 如果 conda 安装在需要权限的位置,考虑重新安装 conda 到用户目录 +``` + +### 问题 5: 安装脚本找不到 conda(方式一) + +**解决方案**: 确保你已经安装了 conda/miniconda/miniforge,并且安装在标准位置: + +- **Windows**: + + - `%USERPROFILE%\miniforge3` + - `%USERPROFILE%\miniconda3` + - `%USERPROFILE%\anaconda3` + - `C:\ProgramData\miniforge3` + +- **macOS/Linux**: + - `~/miniforge3` + - `~/miniconda3` + - `~/anaconda3` + - `/opt/conda` + +如果安装在其他位置,可以先激活 conda base 环境,然后手动运行安装脚本。 + +### 问题 6: 安装后激活环境提示找不到? + +**解决方案**: 尝试以下方法: + +```bash +# 方法 1: 使用 conda activate conda activate unilab -conda remove --force ros-humble-unilabos-msgs -``` -有时相同的安装包版本会由于dev构建得到的md5不一样,触发安全检查,可输入 `config set safety_checks disabled` 来关闭安全检查。 -**安装新版本:** +# 方法 2: 使用完整路径激活(Windows) +call C:\Users\{YourUsername}\miniforge3\envs\unilab\Scripts\activate.bat -访问 https://github.com/dptech-corp/Uni-Lab-OS/actions/workflows/multi-platform-build.yml 选择最新的构建,下载对应平台的压缩包(仅解压一次,得到.conda文件)使用如下指令: -```shell -conda activate base -conda install ros-humble-unilabos-msgs--.conda --offline -n <环境名> +# 方法 2: 使用完整路径激活(Unix) +source ~/miniforge3/envs/unilab/bin/activate ``` -4. **启动 Uni-Lab 系统** +### 问题 7: conda-unpack 失败怎么办?(方式一) -请参见{doc}`启动样例 <../boot_examples/index>`或{doc}`启动指南 `了解详细的启动方法。 +**解决方案**: 尝试手动运行: + +```bash +# Windows +cd %CONDA_PREFIX%\envs\unilab +.\Scripts\conda-unpack.exe + +# macOS/Linux +cd $CONDA_PREFIX/envs/unilab +./bin/conda-unpack +``` + +### 问题 8: 环境很大,有办法减小吗? + +**解决方案**: 预打包的环境包含所有依赖,通常较大(压缩后 2-5GB)。这是为了确保离线安装和完整功能。如果空间有限,考虑使用方式二手动安装,只安装需要的组件。 + +### 问题 9: 如何更新到最新版本? + +**解决方案**: + +**方式一用户**: 重新下载最新的预打包环境,运行安装脚本时选择覆盖现有环境。 + +**方式二/三用户**: 在现有环境中更新: + +```bash +conda activate unilab + +# 更新 unilabos +cd /path/to/Uni-Lab-OS +git pull +pip install -e . --upgrade -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple + +# 更新 ros-humble-unilabos-msgs +mamba update ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge +``` + +--- + +## 下一步 + +安装完成后,请继续: + +- **快速启动**: 学习如何首次启动 Uni-Lab +- **配置指南**: 配置您的实验室环境和设备 +- **运行示例**: 查看启动示例和最佳实践 +- **开发指南**: + - 添加新设备驱动 + - 添加新物料资源 + - 了解工作站架构 + +## 需要帮助? + +- **故障排查**: 查看更详细的故障排查信息 +- **GitHub Issues**: [报告问题](https://github.com/dptech-corp/Uni-Lab-OS/issues) +- **开发者文档**: 查看开发者指南获取更多技术细节 +- **社区讨论**: [GitHub Discussions](https://github.com/dptech-corp/Uni-Lab-OS/discussions) + +--- + +**提示**: + +- 生产环境推荐使用方式二(手动安装)的稳定版本 +- 开发和测试推荐使用方式三(开发者安装) +- 快速体验和演示推荐使用方式一(一键安装) diff --git a/docs/user_guide/launch.md b/docs/user_guide/launch.md index 5691222c..402e39aa 100644 --- a/docs/user_guide/launch.md +++ b/docs/user_guide/launch.md @@ -132,15 +132,14 @@ unilab --config path/to/your/config.py 使用 `-c` 传入控制逻辑配置。 -不管使用哪一种初始化方式,设备/物料字典均需包含 `class` 属性,用于查找注册表信息。默认查找范围都是 Uni-Lab 内部注册表 `unilabos/registry/{devices,device_comms,resources}`。要添加额外的注册表路径,可以使用 `--registry_path` 加入 `/{devices,device_comms,resources}`。 +不管使用哪一种初始化方式,设备/物料字典均需包含 `class` 属性,用于查找注册表信息。默认查找范围都是 Uni-Lab 内部注册表 `unilabos/registry/{devices,device_comms,resources}`。要添加额外的注册表路径,可以使用 `--registry_path` 加入 `/{devices,device_comms,resources}`,只输入即可,支持多次--registry_path指定多个目录。 ## 通信中间件 `--backend` 目前 Uni-Lab 支持以下通信中间件: - **ros** (默认):基于 ROS2 的通信 -- **simple**:简化通信模式 -- **automancer**:Automancer 兼容模式 +- **automancer**:Automancer 兼容模式 (实验性) ## 端云桥接 `--app_bridges` @@ -169,7 +168,7 @@ unilab --config path/to/your/config.py 通过 `--visual` 参数选择: - **rviz**:使用 RViz 进行 3D 可视化 -- **web**:使用 Web 界面进行可视化 +- **web**:使用 Web 界面进行可视化 (基于Pylabrobot) - **disable** (默认):禁用可视化 ## 实验室管理 @@ -245,78 +244,3 @@ unilab --ak your_ak --sk your_sk --port 8080 --disable_browser - 检查图谱文件格式是否正确 - 验证设备连接和端点配置 - 确保注册表路径正确 - -## 页面操作 - -### 1. 启动成功 -当您启动成功后,可以看到物料列表,节点模版和组态图如图展示 -![material.png](image/material.png) - -### 2. 根据需求创建设备和物料 -我们可以做一个简单的案例 -* 在容器1中加入水 -* 通过传输泵将容器1中的水转移到容器2中 -#### 2.1 添加所需的设备和物料 -仪器设备work_station中的workstation 数量x1 -仪器设备virtual_device中的virtual_transfer_pump 数量x1 -物料耗材container中的container 数量x2 - -#### 2.2 将设备和物料根据父子关系进行关联 -当我们添加设备时,仪器耗材模块的物料列表也会实时更新 -我们需要将设备和物料拖拽到workstation中并在画布上将它们连接起来,就像真实的设备操作一样 -![links.png](image/links.png) - -### 3. 创建工作流 -进入工作流模块 → 点击"我创建的" → 新建工作流 -![new.png](image/new.png) - -#### 3.1 新增工作流节点 -我们可以进入指定工作流,在空白处右键 -* 选择Laboratory→host_node中的creat_resource -* 选择Laboratory→workstation中的PumpTransferProtocol - -![creatworkfollow.gif](image/creatworkfollow.gif) - -#### 3.2 配置节点参数 -根据案例,工作流包含两个步骤: -1. 使用creat_resource在容器中创建水 -2. 通过泵传输协议将水传输到另一个容器 - -我们点击creat_resource卡片上的编辑按钮来配置参数⭐️ -class_name :container -device_id : workstation -liquid_input_slot : 0或-1均可 -liquid_type : water -liquid_volume : 根据需求填写即可,默认单位ml,这里举例50 -parent : workstation -res_id : containe -关联设备名称(原unilabos_device_id) : 这里就填写host_node -**配置完成后点击底部保存按钮** - -我们点击PumpTransferProtocol卡片上的编辑按钮来配置参数⭐️ -event : transfer_liquid -from_vessel : water -to_vessel : container1 -volume : 根据需求填写即可,默认单位ml,这里举例50 -关联设备名称(原unilabos_device_id) : 这里就填写workstation -**配置完成后点击底部保存按钮** - -#### 3.3 运行工作流 -1. 连接两个节点卡片 -2. 点击底部保存按钮 -3. 点击运行按钮执行工作流 - -![linksandrun.png](image/linksandrun.png) - -### 运行监控 -* 运行状态和消息实时显示在底部控制台 -* 如有报错,可点击查看详细信息 - -### 结果验证 -工作流完成后,返回仪器耗材模块: -* 点击 container1卡片查看详情 -* 确认其中包含参数指定的水和容量 - - - - diff --git a/docs/user_guide/quick_install_guide.md b/docs/user_guide/quick_install_guide.md deleted file mode 100644 index eefa3ae9..00000000 --- a/docs/user_guide/quick_install_guide.md +++ /dev/null @@ -1,197 +0,0 @@ -# Uni-Lab-OS 一键安装快速指南 - -## 概述 - -本指南提供最快速的 Uni-Lab-OS 安装方法,使用预打包的 conda 环境,无需手动配置依赖。 - -## 前置要求 - -- 已安装 Conda/Miniconda/Miniforge/Mamba -- 至少 10GB 可用磁盘空间 -- Windows 10+, macOS 10.14+, 或 Linux (Ubuntu 20.04+) - -## 安装步骤 - -### 第一步:下载预打包环境 - -1. 访问 [GitHub Actions - Conda Pack Build](https://github.com/dptech-corp/Uni-Lab-OS/actions/workflows/conda-pack-build.yml) - -2. 选择最新的成功构建记录(绿色勾号 ✓) - -3. 在页面底部的 "Artifacts" 部分,下载对应你操作系统的压缩包: - - Windows: `unilab-pack-win-64-{branch}.zip` - - macOS (Intel): `unilab-pack-osx-64-{branch}.tar.gz` - - macOS (Apple Silicon): `unilab-pack-osx-arm64-{branch}.tar.gz` - - Linux: `unilab-pack-linux-64-{branch}.tar.gz` - -### 第二步:解压并运行安装脚本 - -#### Windows - -```batch -REM 使用 Windows 资源管理器解压下载的 zip 文件 -REM 或使用命令行: -tar -xzf unilab-pack-win-64-dev.zip - -REM 进入解压后的目录 -cd unilab-pack-win-64-dev - -REM 双击运行 install_unilab.bat -REM 或在命令行中执行: -install_unilab.bat -``` - -#### macOS - -```bash -# 解压下载的压缩包 -tar -xzf unilab-pack-osx-arm64-dev.tar.gz - -# 进入解压后的目录 -cd unilab-pack-osx-arm64-dev - -# 运行安装脚本 -bash install_unilab.sh -``` - -#### Linux - -```bash -# 解压下载的压缩包 -tar -xzf unilab-pack-linux-64-dev.tar.gz - -# 进入解压后的目录 -cd unilab-pack-linux-64-dev - -# 添加执行权限(如果需要) -chmod +x install_unilab.sh - -# 运行安装脚本 -./install_unilab.sh -``` - -### 第三步:激活环境 - -```bash -conda activate unilab -``` - -### 第四步:验证安装(推荐) - -```bash -# 确保已激活环境 -conda activate unilab - -# 运行验证脚本 -python verify_installation.py -``` - -如果看到 "✓ All checks passed!",说明安装成功! - -## 常见问题 - -### Q: 安装脚本找不到 conda? - -**A:** 确保你已经安装了 conda/miniconda/miniforge,并且安装在标准位置: - -- **Windows**: - - - `%USERPROFILE%\miniforge3` - - `%USERPROFILE%\miniconda3` - - `%USERPROFILE%\anaconda3` - - `C:\ProgramData\miniforge3` - -- **macOS/Linux**: - - `~/miniforge3` - - `~/miniconda3` - - `~/anaconda3` - - `/opt/conda` - -如果安装在其他位置,可以先激活 conda base 环境,然后手动运行安装脚本。 - -### Q: 安装后激活环境提示找不到? - -**A:** 尝试以下方法: - -```bash -# 方法 1: 使用 conda activate -conda activate unilab - -# 方法 2: 使用完整路径激活(Windows) -call C:\Users\{YourUsername}\miniforge3\envs\unilab\Scripts\activate.bat - -# 方法 2: 使用完整路径激活(Unix) -source ~/miniforge3/envs/unilab/bin/activate -``` - -### Q: conda-unpack 失败怎么办? - -**A:** 尝试手动运行: - -```bash -# Windows -cd %CONDA_PREFIX%\envs\unilab -.\Scripts\conda-unpack.exe - -# macOS/Linux -cd $CONDA_PREFIX/envs/unilab -./bin/conda-unpack -``` - -### Q: 验证脚本报错? - -**A:** 首先确认环境已激活: - -```bash -# 检查当前环境 -conda env list - -# 应该看到 unilab 前面有 * 标记 -``` - -如果仍有问题,查看具体报错信息,可能需要: - -- 重新运行安装脚本 -- 检查磁盘空间 -- 查看详细文档 - -### Q: 环境很大,有办法减小吗? - -**A:** 预打包的环境包含所有依赖,通常较大(压缩后 2-5GB)。这是为了确保离线安装和完整功能。如果空间有限,考虑使用手动安装方式,只安装需要的组件。 - -### Q: 如何更新到最新版本? - -**A:** 重新下载最新的预打包环境,运行安装脚本时选择覆盖现有环境。 - -或者在现有环境中更新: - -```bash -conda activate unilab - -# 更新 unilabos -cd /path/to/Uni-Lab-OS -git pull -pip install -e . --upgrade - -# 更新 ros-humble-unilabos-msgs -mamba update ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge -``` - -## 下一步 - -安装完成后,你可以: - -1. **查看启动指南**: {doc}`launch` -2. **运行示例**: {doc}`../boot_examples/index` -3. **配置设备**: 编辑 `unilabos_data/startup_config.json` -4. **阅读开发文档**: {doc}`../developer_guide/workstation_architecture` - -## 需要帮助? - -- **文档**: [docs/user_guide/installation.md](installation.md) -- **问题反馈**: [GitHub Issues](https://github.com/dptech-corp/Uni-Lab-OS/issues) -- **开发版安装**: 参考 {doc}`installation` 的方式二 - ---- - -**提示**: 这个预打包环境包含了从指定分支(通常是 `dev`)构建的最新代码。如果需要稳定版本,请使用方式二手动安装 release 版本。 diff --git a/unilabos/app/main.py b/unilabos/app/main.py index 7b2773db..ad32612d 100644 --- a/unilabos/app/main.py +++ b/unilabos/app/main.py @@ -302,6 +302,11 @@ def main(): graph, resource_tree_set, resource_links = read_node_link_json(request_startup_json) else: file_path = args_dict["graph"] + if not os.path.isfile(file_path): + temp_file_path = os.path.abspath(str(os.path.join(__file__, "..", "..", file_path))) + if os.path.isfile(temp_file_path): + print_status(f"使用相对路径{temp_file_path}", "info") + file_path = temp_file_path if file_path.endswith(".json"): graph, resource_tree_set, resource_links = read_node_link_json(file_path) else: diff --git a/unilabos/devices/liquid_handling/liquid_handler_abstract.py b/unilabos/devices/liquid_handling/liquid_handler_abstract.py index 4142463d..7a492939 100644 --- a/unilabos/devices/liquid_handling/liquid_handler_abstract.py +++ b/unilabos/devices/liquid_handling/liquid_handler_abstract.py @@ -31,15 +31,17 @@ from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode class LiquidHandlerMiddleware(LiquidHandler): - def __init__(self, backend: LiquidHandlerBackend, deck: Deck, simulator: bool = False, channel_num: int = 8, total_height: float = 310, **kwargs): + def __init__(self, backend: LiquidHandlerBackend, deck: Deck, simulator: bool = False, channel_num: int = 8, **kwargs): self._simulator = simulator self.channel_num = channel_num joint_config = kwargs.get("joint_config", None) if simulator: - self._simulate_backend = UniLiquidHandlerRvizBackend(channel_num,total_height, joint_config=joint_config, lh_device_id = deck.name) + if joint_config: + self._simulate_backend = UniLiquidHandlerRvizBackend(channel_num, kwargs["total_height"], + joint_config=joint_config, lh_device_id=deck.name) + else: + self._simulate_backend = LiquidHandlerChatterboxBackend(channel_num) self._simulate_handler = LiquidHandlerAbstract(self._simulate_backend, deck, False) - if hasattr(backend, "total_height"): - backend.total_height = total_height super().__init__(backend, deck) async def setup(self, **backend_kwargs): @@ -544,51 +546,16 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware): support_touch_tip = True _ros_node: BaseROS2DeviceNode - def __init__(self, backend: LiquidHandlerBackend, deck: Deck, simulator: bool=False, channel_num:int = 8,total_height: float = 310,**backend_kwargs): + def __init__(self, backend: LiquidHandlerBackend, deck: Deck, simulator: bool=False, channel_num:int = 8): """Initialize a LiquidHandler. Args: backend: Backend to use. deck: Deck to use. """ - backend_type = None - if isinstance(backend, dict) and "type" in backend: - backend_dict = backend.copy() - type_str = backend_dict.pop("type") - try: - # Try to get class from string using globals (current module), or fallback to pylabrobot or unilabos namespaces - backend_cls = None - if type_str in globals(): - backend_cls = globals()[type_str] - else: - # Try resolving dotted notation, e.g. "xxx.yyy.ClassName" - components = type_str.split(".") - mod = None - if len(components) > 1: - module_name = ".".join(components[:-1]) - try: - import importlib - mod = importlib.import_module(module_name) - except ImportError: - mod = None - if mod is not None: - backend_cls = getattr(mod, components[-1], None) - if backend_cls is None: - # Try pylabrobot style import (if available) - try: - import pylabrobot - backend_cls = getattr(pylabrobot, type_str, None) - except Exception: - backend_cls = None - if backend_cls is not None and isinstance(backend_cls, type): - backend_type = backend_cls(**backend_dict) # pass the rest of dict as kwargs - except Exception as exc: - raise RuntimeError(f"Failed to convert backend type '{type_str}' to class: {exc}") - else: - backend_type = backend self._simulator = simulator self.group_info = dict() - super().__init__(backend_type, deck, simulator, channel_num,total_height,**backend_kwargs) + super().__init__(backend, deck, simulator, channel_num) def post_init(self, ros_node: BaseROS2DeviceNode): self._ros_node = ros_node diff --git a/unilabos/test/__init__.py b/unilabos/test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/commands/resource_add.md b/unilabos/test/commands/resource_add.md similarity index 100% rename from test/commands/resource_add.md rename to unilabos/test/commands/resource_add.md diff --git a/test/experiments/Grignard_flow_batchreact_single_pumpvalve.json b/unilabos/test/experiments/Grignard_flow_batchreact_single_pumpvalve.json similarity index 100% rename from test/experiments/Grignard_flow_batchreact_single_pumpvalve.json rename to unilabos/test/experiments/Grignard_flow_batchreact_single_pumpvalve.json diff --git a/test/experiments/HPLC.json b/unilabos/test/experiments/HPLC.json similarity index 100% rename from test/experiments/HPLC.json rename to unilabos/test/experiments/HPLC.json diff --git a/test/experiments/HT_hiwo.json b/unilabos/test/experiments/HT_hiwo.json similarity index 100% rename from test/experiments/HT_hiwo.json rename to unilabos/test/experiments/HT_hiwo.json diff --git a/test/experiments/ICCAS506.json b/unilabos/test/experiments/ICCAS506.json similarity index 100% rename from test/experiments/ICCAS506.json rename to unilabos/test/experiments/ICCAS506.json diff --git a/test/experiments/Protocol_Test_Station/add_protocol_test_station.json b/unilabos/test/experiments/Protocol_Test_Station/add_protocol_test_station.json similarity index 100% rename from test/experiments/Protocol_Test_Station/add_protocol_test_station.json rename to unilabos/test/experiments/Protocol_Test_Station/add_protocol_test_station.json diff --git a/test/experiments/Protocol_Test_Station/centrifuge_protocol_test_station.json b/unilabos/test/experiments/Protocol_Test_Station/centrifuge_protocol_test_station.json similarity index 100% rename from test/experiments/Protocol_Test_Station/centrifuge_protocol_test_station.json rename to unilabos/test/experiments/Protocol_Test_Station/centrifuge_protocol_test_station.json diff --git a/test/experiments/Protocol_Test_Station/clean_vessel_protocol_test_station.json b/unilabos/test/experiments/Protocol_Test_Station/clean_vessel_protocol_test_station.json similarity index 100% rename from test/experiments/Protocol_Test_Station/clean_vessel_protocol_test_station.json rename to unilabos/test/experiments/Protocol_Test_Station/clean_vessel_protocol_test_station.json diff --git a/test/experiments/Protocol_Test_Station/dual_valve_pump_test_station.json b/unilabos/test/experiments/Protocol_Test_Station/dual_valve_pump_test_station.json similarity index 100% rename from test/experiments/Protocol_Test_Station/dual_valve_pump_test_station.json rename to unilabos/test/experiments/Protocol_Test_Station/dual_valve_pump_test_station.json diff --git a/test/experiments/Protocol_Test_Station/evacuateandrefill_test_station.json b/unilabos/test/experiments/Protocol_Test_Station/evacuateandrefill_test_station.json similarity index 100% rename from test/experiments/Protocol_Test_Station/evacuateandrefill_test_station.json rename to unilabos/test/experiments/Protocol_Test_Station/evacuateandrefill_test_station.json diff --git a/test/experiments/Protocol_Test_Station/evaporate_protocol_test_station.json b/unilabos/test/experiments/Protocol_Test_Station/evaporate_protocol_test_station.json similarity index 100% rename from test/experiments/Protocol_Test_Station/evaporate_protocol_test_station.json rename to unilabos/test/experiments/Protocol_Test_Station/evaporate_protocol_test_station.json diff --git a/test/experiments/Protocol_Test_Station/filter_protocol_test_station.json b/unilabos/test/experiments/Protocol_Test_Station/filter_protocol_test_station.json similarity index 100% rename from test/experiments/Protocol_Test_Station/filter_protocol_test_station.json rename to unilabos/test/experiments/Protocol_Test_Station/filter_protocol_test_station.json diff --git a/test/experiments/Protocol_Test_Station/heatchill_protocol_test_station.json b/unilabos/test/experiments/Protocol_Test_Station/heatchill_protocol_test_station.json similarity index 100% rename from test/experiments/Protocol_Test_Station/heatchill_protocol_test_station.json rename to unilabos/test/experiments/Protocol_Test_Station/heatchill_protocol_test_station.json diff --git a/test/experiments/Protocol_Test_Station/pumptransfer_filterthrough_test_station.json b/unilabos/test/experiments/Protocol_Test_Station/pumptransfer_filterthrough_test_station.json similarity index 100% rename from test/experiments/Protocol_Test_Station/pumptransfer_filterthrough_test_station.json rename to unilabos/test/experiments/Protocol_Test_Station/pumptransfer_filterthrough_test_station.json diff --git a/test/experiments/Protocol_Test_Station/pumptransfer_test_station.json b/unilabos/test/experiments/Protocol_Test_Station/pumptransfer_test_station.json similarity index 100% rename from test/experiments/Protocol_Test_Station/pumptransfer_test_station.json rename to unilabos/test/experiments/Protocol_Test_Station/pumptransfer_test_station.json diff --git a/test/experiments/Protocol_Test_Station/run_column_protocol_test_station.json b/unilabos/test/experiments/Protocol_Test_Station/run_column_protocol_test_station.json similarity index 100% rename from test/experiments/Protocol_Test_Station/run_column_protocol_test_station.json rename to unilabos/test/experiments/Protocol_Test_Station/run_column_protocol_test_station.json diff --git a/test/experiments/Protocol_Test_Station/simple_stir_heatchill_test_station.json b/unilabos/test/experiments/Protocol_Test_Station/simple_stir_heatchill_test_station.json similarity index 100% rename from test/experiments/Protocol_Test_Station/simple_stir_heatchill_test_station.json rename to unilabos/test/experiments/Protocol_Test_Station/simple_stir_heatchill_test_station.json diff --git a/test/experiments/biomek.json b/unilabos/test/experiments/biomek.json similarity index 100% rename from test/experiments/biomek.json rename to unilabos/test/experiments/biomek.json diff --git a/test/experiments/camera.json b/unilabos/test/experiments/camera.json similarity index 100% rename from test/experiments/camera.json rename to unilabos/test/experiments/camera.json diff --git a/test/experiments/comprehensive_protocol/checklist.md b/unilabos/test/experiments/comprehensive_protocol/checklist.md similarity index 100% rename from test/experiments/comprehensive_protocol/checklist.md rename to unilabos/test/experiments/comprehensive_protocol/checklist.md diff --git a/test/experiments/comprehensive_protocol/comprehensive_slim.json b/unilabos/test/experiments/comprehensive_protocol/comprehensive_slim.json similarity index 100% rename from test/experiments/comprehensive_protocol/comprehensive_slim.json rename to unilabos/test/experiments/comprehensive_protocol/comprehensive_slim.json diff --git a/test/experiments/comprehensive_protocol/comprehensive_station.json b/unilabos/test/experiments/comprehensive_protocol/comprehensive_station.json similarity index 100% rename from test/experiments/comprehensive_protocol/comprehensive_station.json rename to unilabos/test/experiments/comprehensive_protocol/comprehensive_station.json diff --git a/test/experiments/deis_control_config.yaml b/unilabos/test/experiments/deis_control_config.yaml similarity index 100% rename from test/experiments/deis_control_config.yaml rename to unilabos/test/experiments/deis_control_config.yaml diff --git a/test/experiments/devices.json b/unilabos/test/experiments/devices.json similarity index 100% rename from test/experiments/devices.json rename to unilabos/test/experiments/devices.json diff --git a/test/experiments/dispensing_station_bioyond.json b/unilabos/test/experiments/dispensing_station_bioyond.json similarity index 100% rename from test/experiments/dispensing_station_bioyond.json rename to unilabos/test/experiments/dispensing_station_bioyond.json diff --git a/test/experiments/empty_devices.json b/unilabos/test/experiments/empty_devices.json similarity index 100% rename from test/experiments/empty_devices.json rename to unilabos/test/experiments/empty_devices.json diff --git a/test/experiments/laiyu_liquid.json b/unilabos/test/experiments/laiyu_liquid.json similarity index 100% rename from test/experiments/laiyu_liquid.json rename to unilabos/test/experiments/laiyu_liquid.json diff --git a/test/experiments/lidocaine-graph.json b/unilabos/test/experiments/lidocaine-graph.json similarity index 100% rename from test/experiments/lidocaine-graph.json rename to unilabos/test/experiments/lidocaine-graph.json diff --git a/test/experiments/mock_devices/mock_all.json b/unilabos/test/experiments/mock_devices/mock_all.json similarity index 100% rename from test/experiments/mock_devices/mock_all.json rename to unilabos/test/experiments/mock_devices/mock_all.json diff --git a/test/experiments/mock_devices/mock_chiller.json b/unilabos/test/experiments/mock_devices/mock_chiller.json similarity index 100% rename from test/experiments/mock_devices/mock_chiller.json rename to unilabos/test/experiments/mock_devices/mock_chiller.json diff --git a/test/experiments/mock_devices/mock_filter.json b/unilabos/test/experiments/mock_devices/mock_filter.json similarity index 100% rename from test/experiments/mock_devices/mock_filter.json rename to unilabos/test/experiments/mock_devices/mock_filter.json diff --git a/test/experiments/mock_devices/mock_heater.json b/unilabos/test/experiments/mock_devices/mock_heater.json similarity index 100% rename from test/experiments/mock_devices/mock_heater.json rename to unilabos/test/experiments/mock_devices/mock_heater.json diff --git a/test/experiments/mock_devices/mock_pump.json b/unilabos/test/experiments/mock_devices/mock_pump.json similarity index 100% rename from test/experiments/mock_devices/mock_pump.json rename to unilabos/test/experiments/mock_devices/mock_pump.json diff --git a/test/experiments/mock_devices/mock_rotavap.json b/unilabos/test/experiments/mock_devices/mock_rotavap.json similarity index 100% rename from test/experiments/mock_devices/mock_rotavap.json rename to unilabos/test/experiments/mock_devices/mock_rotavap.json diff --git a/test/experiments/mock_devices/mock_separator.json b/unilabos/test/experiments/mock_devices/mock_separator.json similarity index 100% rename from test/experiments/mock_devices/mock_separator.json rename to unilabos/test/experiments/mock_devices/mock_separator.json diff --git a/test/experiments/mock_devices/mock_solenoid_valve.json b/unilabos/test/experiments/mock_devices/mock_solenoid_valve.json similarity index 100% rename from test/experiments/mock_devices/mock_solenoid_valve.json rename to unilabos/test/experiments/mock_devices/mock_solenoid_valve.json diff --git a/test/experiments/mock_devices/mock_stirrer.json b/unilabos/test/experiments/mock_devices/mock_stirrer.json similarity index 100% rename from test/experiments/mock_devices/mock_stirrer.json rename to unilabos/test/experiments/mock_devices/mock_stirrer.json diff --git a/test/experiments/mock_devices/mock_stirrer_new.json b/unilabos/test/experiments/mock_devices/mock_stirrer_new.json similarity index 100% rename from test/experiments/mock_devices/mock_stirrer_new.json rename to unilabos/test/experiments/mock_devices/mock_stirrer_new.json diff --git a/test/experiments/mock_devices/mock_vacuum.json b/unilabos/test/experiments/mock_devices/mock_vacuum.json similarity index 100% rename from test/experiments/mock_devices/mock_vacuum.json rename to unilabos/test/experiments/mock_devices/mock_vacuum.json diff --git a/test/experiments/mock_protocol/addteststation.json b/unilabos/test/experiments/mock_protocol/addteststation.json similarity index 100% rename from test/experiments/mock_protocol/addteststation.json rename to unilabos/test/experiments/mock_protocol/addteststation.json diff --git a/test/experiments/mock_protocol/centrifugeteststation.json b/unilabos/test/experiments/mock_protocol/centrifugeteststation.json similarity index 100% rename from test/experiments/mock_protocol/centrifugeteststation.json rename to unilabos/test/experiments/mock_protocol/centrifugeteststation.json diff --git a/test/experiments/mock_protocol/cleanvesselteststation.json b/unilabos/test/experiments/mock_protocol/cleanvesselteststation.json similarity index 100% rename from test/experiments/mock_protocol/cleanvesselteststation.json rename to unilabos/test/experiments/mock_protocol/cleanvesselteststation.json diff --git a/test/experiments/mock_protocol/dissolveteststation.json b/unilabos/test/experiments/mock_protocol/dissolveteststation.json similarity index 100% rename from test/experiments/mock_protocol/dissolveteststation.json rename to unilabos/test/experiments/mock_protocol/dissolveteststation.json diff --git a/test/experiments/mock_protocol/filterteststation.json b/unilabos/test/experiments/mock_protocol/filterteststation.json similarity index 100% rename from test/experiments/mock_protocol/filterteststation.json rename to unilabos/test/experiments/mock_protocol/filterteststation.json diff --git a/test/experiments/mock_protocol/filterthroughteststation.json b/unilabos/test/experiments/mock_protocol/filterthroughteststation.json similarity index 100% rename from test/experiments/mock_protocol/filterthroughteststation.json rename to unilabos/test/experiments/mock_protocol/filterthroughteststation.json diff --git a/test/experiments/mock_protocol/heatchillteststation.json b/unilabos/test/experiments/mock_protocol/heatchillteststation.json similarity index 100% rename from test/experiments/mock_protocol/heatchillteststation.json rename to unilabos/test/experiments/mock_protocol/heatchillteststation.json diff --git a/test/experiments/mock_protocol/runcolumnteststation.json b/unilabos/test/experiments/mock_protocol/runcolumnteststation.json similarity index 100% rename from test/experiments/mock_protocol/runcolumnteststation.json rename to unilabos/test/experiments/mock_protocol/runcolumnteststation.json diff --git a/test/experiments/mock_protocol/stirteststation.json b/unilabos/test/experiments/mock_protocol/stirteststation.json similarity index 100% rename from test/experiments/mock_protocol/stirteststation.json rename to unilabos/test/experiments/mock_protocol/stirteststation.json diff --git a/test/experiments/mock_protocol/transferteststation.json b/unilabos/test/experiments/mock_protocol/transferteststation.json similarity index 100% rename from test/experiments/mock_protocol/transferteststation.json rename to unilabos/test/experiments/mock_protocol/transferteststation.json diff --git a/test/experiments/mock_protocol/washsolidteststation.json b/unilabos/test/experiments/mock_protocol/washsolidteststation.json similarity index 100% rename from test/experiments/mock_protocol/washsolidteststation.json rename to unilabos/test/experiments/mock_protocol/washsolidteststation.json diff --git a/test/experiments/mock_reactor.json b/unilabos/test/experiments/mock_reactor.json similarity index 100% rename from test/experiments/mock_reactor.json rename to unilabos/test/experiments/mock_reactor.json diff --git a/test/experiments/opcua_example.json b/unilabos/test/experiments/opcua_example.json similarity index 100% rename from test/experiments/opcua_example.json rename to unilabos/test/experiments/opcua_example.json diff --git a/test/experiments/plr_test.json b/unilabos/test/experiments/plr_test.json similarity index 100% rename from test/experiments/plr_test.json rename to unilabos/test/experiments/plr_test.json diff --git a/test/experiments/plr_test_converted.json b/unilabos/test/experiments/plr_test_converted.json similarity index 100% rename from test/experiments/plr_test_converted.json rename to unilabos/test/experiments/plr_test_converted.json diff --git a/test/experiments/plr_test_converted_slim.json b/unilabos/test/experiments/plr_test_converted_slim.json similarity index 100% rename from test/experiments/plr_test_converted_slim.json rename to unilabos/test/experiments/plr_test_converted_slim.json diff --git a/test/experiments/prcxi_9300.json b/unilabos/test/experiments/prcxi_9300.json similarity index 100% rename from test/experiments/prcxi_9300.json rename to unilabos/test/experiments/prcxi_9300.json diff --git a/test/experiments/prcxi_9320.json b/unilabos/test/experiments/prcxi_9320.json similarity index 100% rename from test/experiments/prcxi_9320.json rename to unilabos/test/experiments/prcxi_9320.json diff --git a/test/experiments/prcxi_9320_visual.json b/unilabos/test/experiments/prcxi_9320_visual.json similarity index 100% rename from test/experiments/prcxi_9320_visual.json rename to unilabos/test/experiments/prcxi_9320_visual.json diff --git a/test/experiments/reaction_station_bioyond.json b/unilabos/test/experiments/reaction_station_bioyond.json similarity index 100% rename from test/experiments/reaction_station_bioyond.json rename to unilabos/test/experiments/reaction_station_bioyond.json diff --git a/test/experiments/reaction_station_bioyond_test.json b/unilabos/test/experiments/reaction_station_bioyond_test.json similarity index 100% rename from test/experiments/reaction_station_bioyond_test.json rename to unilabos/test/experiments/reaction_station_bioyond_test.json diff --git a/test/experiments/test.json b/unilabos/test/experiments/test.json similarity index 100% rename from test/experiments/test.json rename to unilabos/test/experiments/test.json diff --git a/test/experiments/test_copy.json b/unilabos/test/experiments/test_copy.json similarity index 100% rename from test/experiments/test_copy.json rename to unilabos/test/experiments/test_copy.json diff --git a/test/experiments/test_laiyu.json b/unilabos/test/experiments/test_laiyu.json similarity index 100% rename from test/experiments/test_laiyu.json rename to unilabos/test/experiments/test_laiyu.json diff --git a/test/experiments/test_laiyu_v.json b/unilabos/test/experiments/test_laiyu_v.json similarity index 100% rename from test/experiments/test_laiyu_v.json rename to unilabos/test/experiments/test_laiyu_v.json diff --git a/test/experiments/test_moveit.json b/unilabos/test/experiments/test_moveit.json similarity index 100% rename from test/experiments/test_moveit.json rename to unilabos/test/experiments/test_moveit.json diff --git a/test/experiments/workshop.json b/unilabos/test/experiments/workshop.json similarity index 100% rename from test/experiments/workshop.json rename to unilabos/test/experiments/workshop.json diff --git a/unilabos/test/registry/__init__.py b/unilabos/test/registry/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/registry/example_devices.py b/unilabos/test/registry/example_devices.py similarity index 100% rename from test/registry/example_devices.py rename to unilabos/test/registry/example_devices.py diff --git a/unilabos/test/resources/__init__.py b/unilabos/test/resources/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/resources/bioyond_materials_liquidhandling_1.json b/unilabos/test/resources/bioyond_materials_liquidhandling_1.json similarity index 100% rename from test/resources/bioyond_materials_liquidhandling_1.json rename to unilabos/test/resources/bioyond_materials_liquidhandling_1.json diff --git a/test/resources/bioyond_materials_liquidhandling_2.json b/unilabos/test/resources/bioyond_materials_liquidhandling_2.json similarity index 100% rename from test/resources/bioyond_materials_liquidhandling_2.json rename to unilabos/test/resources/bioyond_materials_liquidhandling_2.json diff --git a/test/resources/bioyond_materials_reaction.json b/unilabos/test/resources/bioyond_materials_reaction.json similarity index 100% rename from test/resources/bioyond_materials_reaction.json rename to unilabos/test/resources/bioyond_materials_reaction.json diff --git a/test/resources/test_bottle_carrier.py b/unilabos/test/resources/test_bottle_carrier.py similarity index 100% rename from test/resources/test_bottle_carrier.py rename to unilabos/test/resources/test_bottle_carrier.py diff --git a/test/resources/test_converter_bioyond.py b/unilabos/test/resources/test_converter_bioyond.py similarity index 100% rename from test/resources/test_converter_bioyond.py rename to unilabos/test/resources/test_converter_bioyond.py diff --git a/test/resources/test_itemized_carrier.py b/unilabos/test/resources/test_itemized_carrier.py similarity index 100% rename from test/resources/test_itemized_carrier.py rename to unilabos/test/resources/test_itemized_carrier.py diff --git a/test/resources/test_resourcetreeset.py b/unilabos/test/resources/test_resourcetreeset.py similarity index 100% rename from test/resources/test_resourcetreeset.py rename to unilabos/test/resources/test_resourcetreeset.py diff --git a/unilabos/test/ros/__init__.py b/unilabos/test/ros/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/ros/msgs/__init__.py b/unilabos/test/ros/msgs/__init__.py similarity index 100% rename from test/ros/msgs/__init__.py rename to unilabos/test/ros/msgs/__init__.py diff --git a/test/ros/msgs/test_basic.py b/unilabos/test/ros/msgs/test_basic.py similarity index 100% rename from test/ros/msgs/test_basic.py rename to unilabos/test/ros/msgs/test_basic.py diff --git a/test/ros/msgs/test_conversion.py b/unilabos/test/ros/msgs/test_conversion.py similarity index 100% rename from test/ros/msgs/test_conversion.py rename to unilabos/test/ros/msgs/test_conversion.py diff --git a/test/ros/msgs/test_mapping.py b/unilabos/test/ros/msgs/test_mapping.py similarity index 100% rename from test/ros/msgs/test_mapping.py rename to unilabos/test/ros/msgs/test_mapping.py diff --git a/test/ros/msgs/test_runner.py b/unilabos/test/ros/msgs/test_runner.py similarity index 100% rename from test/ros/msgs/test_runner.py rename to unilabos/test/ros/msgs/test_runner.py diff --git a/unilabos/test/workflow/__init__.py b/unilabos/test/workflow/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/workflow/example_bio.json b/unilabos/test/workflow/example_bio.json similarity index 100% rename from test/workflow/example_bio.json rename to unilabos/test/workflow/example_bio.json diff --git a/test/workflow/example_bio_graph.png b/unilabos/test/workflow/example_bio_graph.png similarity index 100% rename from test/workflow/example_bio_graph.png rename to unilabos/test/workflow/example_bio_graph.png diff --git a/test/workflow/example_prcxi.json b/unilabos/test/workflow/example_prcxi.json similarity index 100% rename from test/workflow/example_prcxi.json rename to unilabos/test/workflow/example_prcxi.json diff --git a/test/workflow/example_prcxi_graph.png b/unilabos/test/workflow/example_prcxi_graph.png similarity index 100% rename from test/workflow/example_prcxi_graph.png rename to unilabos/test/workflow/example_prcxi_graph.png diff --git a/test/workflow/example_prcxi_graph_20251022_1359.png b/unilabos/test/workflow/example_prcxi_graph_20251022_1359.png similarity index 100% rename from test/workflow/example_prcxi_graph_20251022_1359.png rename to unilabos/test/workflow/example_prcxi_graph_20251022_1359.png diff --git a/test/workflow/merge_workflow.py b/unilabos/test/workflow/merge_workflow.py similarity index 100% rename from test/workflow/merge_workflow.py rename to unilabos/test/workflow/merge_workflow.py