Compare commits

...

2 Commits

Author SHA1 Message Date
Xuwznln
75f09034ff update docs, test examples
fix liquid_handler init bug
2025-11-18 18:42:27 +08:00
ZiWei
549a50220b fix camera & workstation & warehouse & reaction station driver 2025-11-18 18:41:37 +08:00
127 changed files with 8474 additions and 1167 deletions

View File

@@ -1,3 +1,4 @@
recursive-include unilabos/test *
recursive-include unilabos/registry *.yaml recursive-include unilabos/registry *.yaml
recursive-include unilabos/app/web/static * recursive-include unilabos/app/web/static *
recursive-include unilabos/app/web/templates * recursive-include unilabos/app/web/templates *

View File

@@ -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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 KiB

View File

@@ -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)

View File

@@ -1,7 +1,7 @@
(instructions)= (instructions)=
# 设备抽象、指令集与通信中间件 # 设备抽象、指令集与通信中间件
Uni-Lab 操作系统的目的是将不同类型和厂家的实验仪器进行抽象统一,对应用层提供服务。因此,理清实验室设备之间的业务逻辑至关重要。 Uni-Lab-OS的目的是将不同类型和厂家的实验仪器进行抽象统一,对应用层提供服务。因此,理清实验室设备之间的业务逻辑至关重要。
## 设备间通信模式 ## 设备间通信模式

File diff suppressed because it is too large Load Diff

View File

@@ -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 ```python
class MockGripper: class MockGripper:
@@ -31,12 +39,11 @@ class MockGripper:
def status(self) -> str: def status(self) -> str:
return self._status return self._status
# 会被自动识别的设备动作,接入 Uni-Lab 时会作为 ActionServer 接受任意控制者的指令
@status.setter @status.setter
def status(self, target): def status(self, target):
self._status = target self._status = target
# 需要在注册表添加的设备动作,接入 Uni-Lab 时会作为 ActionServer 接受任意控制者的指令 # 会被自动识别的设备动作,接入 Uni-Lab 时会作为 ActionServer 接受任意控制者的指令
def push_to(self, position: float, torque: float, velocity: float = 0.0): def push_to(self, position: float, torque: float, velocity: float = 0.0):
self._status = "Running" self._status = "Running"
current_pos = self.position current_pos = self.position
@@ -53,9 +60,11 @@ class MockGripper:
self._status = "Idle" self._status = "Idle"
``` ```
Python 类设备驱动在完成注册表后可以直接在 Uni-Lab 使用。 ### 2. C# Class
2. C# Class C# 驱动设备在完成注册表后,需要调用 Uni-Lab C# 编译后才能使用(仅需一次)。
**示例:**
```csharp ```csharp
using System; using System;
@@ -84,7 +93,7 @@ public class MockGripper
position = currentPos + (Position - currentPos) / 20 * (i + 1); position = currentPos + (Position - currentPos) / 20 * (i + 1);
torque = Torque / (20 - i); torque = Torque / (20 - i);
velocity = Velocity; velocity = Velocity;
await Task.Delay((int)(moveTime * 1000 / 20)); // Convert seconds to milliseconds await Task.Delay((int)(moveTime * 1000 / 20));
} }
torque = Torque; torque = Torque;
status = "Idle"; status = "Idle";
@@ -92,12 +101,16 @@ public class MockGripper
} }
``` ```
C# 驱动设备在完成注册表后,需要调用 Uni-Lab C# 编译后才能使用,但只需一次。 ---
## 快速开始:使用注册表编辑器(推荐) ## 快速开始:两种方式添加设备
### 方式 1使用注册表编辑器推荐
推荐使用 Uni-Lab-OS 自带的可视化编辑器,它能自动分析您的设备驱动并生成大部分配置: 推荐使用 Uni-Lab-OS 自带的可视化编辑器,它能自动分析您的设备驱动并生成大部分配置:
**步骤:**
1. 启动 Uni-Lab-OS 1. 启动 Uni-Lab-OS
2. 在浏览器中打开"注册表编辑器"页面 2. 在浏览器中打开"注册表编辑器"页面
3. 选择您的 Python 设备驱动文件 3. 选择您的 Python 设备驱动文件
@@ -106,13 +119,18 @@ C# 驱动设备在完成注册表后,需要调用 Uni-Lab C# 编译后才能
6. 点击"生成注册表",复制生成的内容 6. 点击"生成注册表",复制生成的内容
7. 保存到 `devices/` 目录下 7. 保存到 `devices/` 目录下
--- **优点:**
## 手动编写注册表(简化版) - 自动识别设备属性和方法
- 可视化界面,易于操作
- 自动生成完整配置
- 减少手动配置错误
### 方式 2手动编写注册表简化版
如果需要手动编写,只需要提供两个必需字段,系统会自动补全其余内容: 如果需要手动编写,只需要提供两个必需字段,系统会自动补全其余内容:
### 最小配置示例 **最小配置示例**
```yaml ```yaml
my_device: # 设备唯一标识符 my_device: # 设备唯一标识符
@@ -121,22 +139,22 @@ my_device: # 设备唯一标识符
type: python # 驱动类型 type: python # 驱动类型
``` ```
### 注册表文件位置 **注册表文件位置**
- 默认路径:`unilabos/registry/devices` - 默认路径:`unilabos/registry/devices`
- 自定义路径:启动时使用 `--registry` 参数指定 - 自定义路径:启动时使用 `--registry_path` 参数指定
- 可将多个设备写在同一个 yaml 文件中 - 可将多个设备写在同一个 YAML 文件中
### 系统自动生成的内容 **系统自动生成的内容**
系统会自动分析您的 Python 驱动类并生成: 系统会自动分析您的 Python 驱动类并生成:
- `status_types`:从 `get_*` 方法自动识别状态属性 - `status_types`:从 `@property` 装饰的方法自动识别状态属性
- `action_value_mappings`:从类方法自动生成动作映射 - `action_value_mappings`:从类方法自动生成动作映射
- `init_param_schema`:从 `__init__` 方法分析初始化参数 - `init_param_schema`:从 `__init__` 方法分析初始化参数
- `schema`:前端显示用的属性类型定义 - `schema`:前端显示用的属性类型定义
### 完整结构概览 **完整结构概览**
```yaml ```yaml
my_device: my_device:
@@ -151,4 +169,848 @@ my_device:
schema: {} # 自动生成 schema: {} # 自动生成
``` ```
详细的注册表编写指南和高级配置,请参考{doc}`yaml 注册表编写指南 <add_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.sleepROS2 异步机制)
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 提供的方法。

View File

@@ -1,8 +1,10 @@
# 设备 Driver 开发 # 设备 Driver 开发(无 SDK 设备)
我们对设备 Driver 的定义,是一个 Python/C++/C# 类,类的方法可以用于获取传感器数据、执行设备动作、更新物料信息。它们经过 Uni-Lab 的通信中间件包装,就能成为高效分布式通信的设备节点。 我们对设备 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 ## 有串口字符串指令集文档的设备Python 串口通信(常见 RS485, RS232, USB
@@ -12,13 +14,13 @@
Modbus 与 RS485、RS232 不一样的地方在于会有更多直接寄存器的读写以及涉及字节序转换Big Endian, Little Endian 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` - 单一机械设备**电夹爪**,通讯协议可见 [增广夹爪通讯协议](https://doc.rmaxis.com/docs/communication/fieldbus/),驱动代码位于 `unilabos/devices/gripper/rmaxis_v4.py`
* 单一通信设备**IO板卡**,驱动代码位于 `unilabos/device_comms/gripper/SRND_16_IO.py` - 单一通信设备**IO 板卡**,驱动代码位于 `unilabos/device_comms/gripper/SRND_16_IO.py`
* 执行多设备复杂任务逻辑的**PLC**Uni-Lab 提供了基于地址表的接入方式和点动工作流编写,测试代码位于 `unilabos/device_comms/modbus_plc/test/test_workflow.py` - 执行多设备复杂任务逻辑的**PLC**Uni-Lab 提供了基于地址表的接入方式和点动工作流编写,测试代码位于 `unilabos/device_comms/modbus_plc/test/test_workflow.py`
**** ---
## 其他工业通信协议CANopen, Ethernet, OPCUA... ## 其他工业通信协议CANopen, Ethernet, OPCUA...
@@ -26,32 +28,32 @@ Uni-Lab 开发团队在仓库中提供了3个样例
## 没有接口的老设备老软件:使用 PyWinAuto ## 没有接口的老设备老软件:使用 PyWinAuto
**pywinauto**是一个 Python 库用于自动化Windows GUI操作。它可以模拟用户的鼠标点击、键盘输入、窗口操作等广泛应用于自动化测试、GUI自动化等场景。它支持通过两个后端进行操作 **pywinauto**是一个 Python 库,用于自动化 Windows GUI 操作。它可以模拟用户的鼠标点击、键盘输入、窗口操作等广泛应用于自动化测试、GUI 自动化等场景。它支持通过两个后端进行操作:
* **win32**后端适用于大多数Windows应用程序使用native Win32 API。pywinauto_recorder默认使用win32后端 - **win32**后端:适用于大多数 Windows 应用程序,使用 native Win32 API。pywinauto_recorder 默认使用 win32 后端)
* **uia**后端基于Microsoft UI Automation适用于较新的应用程序特别是基于WPFUWP的应用程序。在win10上会有更全的目录有的窗口win32会识别不到 - **uia**后端:基于 Microsoft UI Automation适用于较新的应用程序特别是基于 WPFUWP 的应用程序。(在 win10 上,会有更全的目录,有的窗口 win32 会识别不到)
### windows平台安装pywinautopywinauto_recorder ### windows 平台安装 pywinautopywinauto_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) ![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 查找需要的窗口
* 获取某个位置的信息 - 获取某个位置的信息
* 模拟点击 - 模拟点击
* 模拟输入 - 模拟输入
#### 代码学习 #### 代码学习
@@ -262,13 +264,13 @@ r, g, b = pyautogui.pixel(point_x, point_y)
### pywinauto_recorder ### 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) ![calculator_01](image/device_driver/calculator_01.png)
@@ -286,7 +288,7 @@ with UIPath(u"计算器||Window"):
click(u"九||Button") click(u"九||Button")
``` ```
执行该python脚本可以观察到新开启的计算器被点击了数字9 执行该 python 脚本,可以观察到新开启的计算器被点击了数字 9
![calculator_03](image/device_driver/calculator_03.png) ![calculator_03](image/device_driver/calculator_03.png)
@@ -315,16 +317,31 @@ child_window(title="数字键盘", auto_id="NumberPad", control_type="Group")
""" """
``` ```
这里以上面计算器的例子对dump_tree进行解读 这里以上面计算器的例子对 dump_tree 进行解读
2~4行为当前对象的窗口 2~4 行为当前对象的窗口
*2行分别是窗体的类型 `GroupBox`,窗体的题目 `数字键盘`,窗体的矩形区域坐标,对应的是屏幕上的位置(左、上、右、下) - 2 行分别是窗体的类型 `GroupBox`,窗体的题目 `数字键盘`,窗体的矩形区域坐标,对应的是屏幕上的位置(左、上、右、下)
*3行是 `['GroupBox', '数字键盘', '数字键盘GroupBox']`,为控件的标识符列表,可以选择任意一个,使用 `child_window(best_match="标识符")`来获取该窗口 - 3 行是 `['GroupBox', '数字键盘', '数字键盘GroupBox']`,为控件的标识符列表,可以选择任意一个,使用 `child_window(best_match="标识符")`来获取该窗口
*4行是获取该控件的方法,请注意该方法不能保证获取唯一,`title`如果是变化的,也需要删除 `title`参数 - 4 行是获取该控件的方法,请注意该方法不能保证获取唯一,`title`如果是变化的,也需要删除 `title`参数
6~8行为当前对象窗口所包含的子窗口信息信息类型对应2~4行 6~8 行为当前对象窗口所包含的子窗口信息,信息类型对应 2~4
### 窗口获取注意事项 ### 窗口获取注意事项
1. 在 `child_window`的时候,并不会立刻报错,只有在执行窗口的信息获取时才会调用,查询窗口是否存在,因此要想确定 `child_window`是否正确,可以调用子窗口对象的属性 `element_info`,来保证窗口存在 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` - 设备测试指南

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,17 @@
# 电池装配工站接入PLC # 实例:电池装配工站接入PLC控制
本指南将引导你完成电池装配工站(以 PLC 控制为例)的接入流程,包括新建工站文件、编写驱动与寄存器读写、生成注册表、上传及注意事项。 > **文档类型**:实际应用案例
> **适用场景**:使用 PLC 控制的电池装配工站接入
> **前置知识**{doc}`../add_device` | {doc}`../add_registry`
本指南以电池装配工站为实际案例,引导你完成 PLC 控制设备的完整接入流程,包括新建工站文件、编写驱动与寄存器读写、生成注册表、上传及注意事项。
## 案例概述
**设备类型**:电池装配工站
**通信方式**Modbus TCP (PLC)
**工站基类**`WorkstationBase`
**主要功能**:电池组装、寄存器读写、数据采集
## 1. 新建工站文件 ## 1. 新建工站文件
@@ -93,10 +104,12 @@ python unilabos\app\main.py -g celljson.json --ak <user的AK> --sk <user的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`文件 1. 选择新增的工站`coin_cell_assembly.py`文件
@@ -107,8 +120,9 @@ python unilabos\app\main.py -g celljson.json --ak <user的AK> --sk <user的SK>
6. 填写新的工站注册表备注信息 6. 填写新的工站注册表备注信息
7. 生成注册表 7. 生成注册表
以上操作步骤完成,则会生成的新的注册表ymal文件,如下图: 以上操作步骤完成,则会生成的新的注册表YAML文件,如下图:
![Layers](image_add_batteryPLC/unilab_new_yaml.png)
![生成的YAML文件](image_battery_plc/unilab_new_yaml.png)
@@ -134,14 +148,60 @@ python unilabos\app\main.py -g celljson.json --ak <user的AK> --sk <user的SK> -
## 4. 注意事项 ## 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 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 设备接入流程,可以作为其他类似设备接入的参考模板。

View File

Before

Width:  |  Height:  |  Size: 428 KiB

After

Width:  |  Height:  |  Size: 428 KiB

View File

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -1,4 +1,8 @@
# 物料构建指南 # 实例:物料构建指南
> **文档类型**:物料系统实战指南
> **适用场景**工作站物料系统构建、Deck/Warehouse/Carrier/Bottle 配置
> **前置知识**PyLabRobot 基础 | 资源管理概念
## 概述 ## 概述

View File

@@ -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 结构表现形式及使用场景。
--- ---

View File

@@ -1,4 +1,8 @@
# 工作站模板架构设计与对接指南 # 实例:工作站模板架构设计与对接指南
> **文档类型**:架构设计指南与实战案例
> **适用场景**:大型工作站接入、子设备管理、物料系统集成
> **前置知识**{doc}`../add_device` | {doc}`../add_registry`
## 0. 问题简介 ## 0. 问题简介
@@ -6,9 +10,9 @@
### 0.1 自研常量有机工站:最重要的是子设备管理和通信转发 ### 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 移液工作站:物料系统和工作流模板管理 ### 0.2 移液工作站:物料系统和工作流模板管理
![workstation_liquid_handler](image/workstation_architecture/workstation_liquid_handler.png) ![workstation_liquid_handler](../image/workstation_architecture/workstation_liquid_handler.png)
1. 绝大多数情况没有子设备,有时候选配恒温震荡等模块时,接口也由工作站提供 1. 绝大多数情况没有子设备,有时候选配恒温震荡等模块时,接口也由工作站提供
2. 所有任务系统均由工作站本身实现并下发指令有统一的抽象函数可实现pick_up_tips, aspirate, dispense, transfer 等)。有时需要将这些指令组合、转化为工作站的脚本语言,再统一下发。因此会形成大量固定的 protocols。 2. 所有任务系统均由工作站本身实现并下发指令有统一的抽象函数可实现pick_up_tips, aspirate, dispense, transfer 等)。有时需要将这些指令组合、转化为工作站的脚本语言,再统一下发。因此会形成大量固定的 protocols。
@@ -26,7 +30,7 @@
### 0.3 厂家开发的定制大型工站 ### 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 协议统一通信 由厂家开发,具备完善的物料系统、任务系统甚至调度系统;由 PLC 或 OpenAPI TCP 协议统一通信

View File

@@ -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 <slave_node_ip>
# 设置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 <target_ip>
# 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 <host_ip>
```
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 <node_ip>
# 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 云平台文档

View File

@@ -1,6 +1,6 @@
# Uni-Lab 项目文档 # Uni-Lab-OS 项目文档
欢迎来到项目文档的首页! Uni-Lab-OS 是一个开源的实验室自动化操作系统,提供统一的设备接口、工作流管理和分布式部署能力。
```{toctree} ```{toctree}
:maxdepth: 3 :maxdepth: 3

View File

@@ -10,35 +10,51 @@ concepts/01-communication-instruction.md
concepts/02-topology-and-chemputer-compile.md concepts/02-topology-and-chemputer-compile.md
``` ```
## **用户指南** ## 用户指南
本指南将带你了解如何使用项目的功能 快速上手、系统配置与使用说明
```{toctree} ```{toctree}
:maxdepth: 2 :maxdepth: 2
user_guide/best_practice.md
user_guide/installation.md user_guide/installation.md
user_guide/configuration.md
user_guide/launch.md user_guide/launch.md
user_guide/graph_files.md
boot_examples/index.md boot_examples/index.md
``` ```
## 进阶配置
高级配置和系统管理。
```{toctree}
:maxdepth: 2
advanced_usage/configuration.md
advanced_usage/working_directory.md
```
## 开发者指南 ## 开发者指南
```{toctree} 设备开发、系统扩展与架构说明。
```{toctree}
:maxdepth: 2 :maxdepth: 2
developer_guide/device_driver developer_guide/networking_overview.md
developer_guide/add_device developer_guide/add_device.md
developer_guide/add_action developer_guide/add_old_device.md
developer_guide/actions developer_guide/add_registry.md
developer_guide/workstation_architecture developer_guide/add_yaml.md
developer_guide/add_protocol developer_guide/add_action.md
developer_guide/add_batteryPLC developer_guide/actions.md
developer_guide/materials_tutorial developer_guide/action_includes.md
developer_guide/materials_construction_guide 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
``` ```
## 接口文档 ## 接口文档

File diff suppressed because it is too large Load Diff

View File

@@ -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` 参数时会看到提示信息
- 检查启动日志中的配置加载信息
- 临时移除低优先级配置来测试高优先级配置是否生效

View File

@@ -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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -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 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 ```bash
pip install -e . # 查看当前 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: 权限错误
**卸载老版本:** **Windows 解决方案**: 以管理员身份运行命令提示符
```shell
**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 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文件使用如下指令 # 方法 2: 使用完整路径激活Unix
```shell source ~/miniforge3/envs/unilab/bin/activate
conda activate base
conda install ros-humble-unilabos-msgs-<version>-<platform>.conda --offline -n <环境名>
``` ```
4. **启动 Uni-Lab 系统** ### 问题 7: conda-unpack 失败怎么办?(方式一)
请参见{doc}`启动样例 <../boot_examples/index>`或{doc}`启动指南 <launch>`了解详细的启动方法。 **解决方案**: 尝试手动运行:
```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)
---
**提示**:
- 生产环境推荐使用方式二(手动安装)的稳定版本
- 开发和测试推荐使用方式三(开发者安装)
- 快速体验和演示推荐使用方式一(一键安装)

View File

@@ -132,15 +132,14 @@ unilab --config path/to/your/config.py
使用 `-c` 传入控制逻辑配置。 使用 `-c` 传入控制逻辑配置。
不管使用哪一种初始化方式,设备/物料字典均需包含 `class` 属性,用于查找注册表信息。默认查找范围都是 Uni-Lab 内部注册表 `unilabos/registry/{devices,device_comms,resources}`。要添加额外的注册表路径,可以使用 `--registry_path` 加入 `<your-registry-path>/{devices,device_comms,resources}` 不管使用哪一种初始化方式,设备/物料字典均需包含 `class` 属性,用于查找注册表信息。默认查找范围都是 Uni-Lab 内部注册表 `unilabos/registry/{devices,device_comms,resources}`。要添加额外的注册表路径,可以使用 `--registry_path` 加入 `<your-registry-path>/{devices,device_comms,resources}`,只输入<your-registry-path>即可,支持多次--registry_path指定多个目录
## 通信中间件 `--backend` ## 通信中间件 `--backend`
目前 Uni-Lab 支持以下通信中间件: 目前 Uni-Lab 支持以下通信中间件:
- **ros** (默认):基于 ROS2 的通信 - **ros** (默认):基于 ROS2 的通信
- **simple**:简化通信模式 - **automancer**Automancer 兼容模式 (实验性)
- **automancer**Automancer 兼容模式
## 端云桥接 `--app_bridges` ## 端云桥接 `--app_bridges`
@@ -169,7 +168,7 @@ unilab --config path/to/your/config.py
通过 `--visual` 参数选择: 通过 `--visual` 参数选择:
- **rviz**:使用 RViz 进行 3D 可视化 - **rviz**:使用 RViz 进行 3D 可视化
- **web**:使用 Web 界面进行可视化 - **web**:使用 Web 界面进行可视化 (基于Pylabrobot)
- **disable** (默认):禁用可视化 - **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卡片查看详情
* 确认其中包含参数指定的水和容量

View File

@@ -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 版本。

View File

@@ -302,6 +302,11 @@ def main():
graph, resource_tree_set, resource_links = read_node_link_json(request_startup_json) graph, resource_tree_set, resource_links = read_node_link_json(request_startup_json)
else: else:
file_path = args_dict["graph"] 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"): if file_path.endswith(".json"):
graph, resource_tree_set, resource_links = read_node_link_json(file_path) graph, resource_tree_set, resource_links = read_node_link_json(file_path)
else: else:

View File

@@ -31,15 +31,17 @@ from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
class LiquidHandlerMiddleware(LiquidHandler): 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._simulator = simulator
self.channel_num = channel_num self.channel_num = channel_num
joint_config = kwargs.get("joint_config", None) joint_config = kwargs.get("joint_config", None)
if simulator: 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) self._simulate_handler = LiquidHandlerAbstract(self._simulate_backend, deck, False)
if hasattr(backend, "total_height"):
backend.total_height = total_height
super().__init__(backend, deck) super().__init__(backend, deck)
async def setup(self, **backend_kwargs): async def setup(self, **backend_kwargs):
@@ -544,51 +546,16 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
support_touch_tip = True support_touch_tip = True
_ros_node: BaseROS2DeviceNode _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. """Initialize a LiquidHandler.
Args: Args:
backend: Backend to use. backend: Backend to use.
deck: Deck 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._simulator = simulator
self.group_info = dict() 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): def post_init(self, ros_node: BaseROS2DeviceNode):
self._ros_node = ros_node self._ros_node = ros_node

View File

@@ -1,7 +1,11 @@
import json import json
import time
import requests import requests
from typing import List, Dict, Any from typing import List, Dict, Any
from pathlib import Path
from datetime import datetime
from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation
from unilabos.ros.msgs.message_converter import convert_to_ros_msg, Float64, String
from unilabos.devices.workstation.bioyond_studio.config import ( from unilabos.devices.workstation.bioyond_studio.config import (
WORKFLOW_STEP_IDS, WORKFLOW_STEP_IDS,
WORKFLOW_TO_SECTION_MAP, WORKFLOW_TO_SECTION_MAP,
@@ -10,6 +14,37 @@ from unilabos.devices.workstation.bioyond_studio.config import (
from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG
class BioyondReactor:
def __init__(self, config: dict = None, deck=None, protocol_type=None, **kwargs):
self.in_temperature = 0.0
self.out_temperature = 0.0
self.pt100_temperature = 0.0
self.sensor_average_temperature = 0.0
self.target_temperature = 0.0
self.setting_temperature = 0.0
self.viscosity = 0.0
self.average_viscosity = 0.0
self.speed = 0.0
self.force = 0.0
def update_metrics(self, payload: Dict[str, Any]):
def _f(v):
try:
return float(v)
except Exception:
return 0.0
self.target_temperature = _f(payload.get("targetTemperature"))
self.setting_temperature = _f(payload.get("settingTemperature"))
self.in_temperature = _f(payload.get("inTemperature"))
self.out_temperature = _f(payload.get("outTemperature"))
self.pt100_temperature = _f(payload.get("pt100Temperature"))
self.sensor_average_temperature = _f(payload.get("sensorAverageTemperature"))
self.speed = _f(payload.get("speed"))
self.force = _f(payload.get("force"))
self.viscosity = _f(payload.get("viscosity"))
self.average_viscosity = _f(payload.get("averageViscosity"))
class BioyondReactionStation(BioyondWorkstation): class BioyondReactionStation(BioyondWorkstation):
"""Bioyond反应站类 """Bioyond反应站类
@@ -37,6 +72,19 @@ class BioyondReactionStation(BioyondWorkstation):
print(f"BioyondReactionStation初始化完成 - workflow_mappings: {self.workflow_mappings}") print(f"BioyondReactionStation初始化完成 - workflow_mappings: {self.workflow_mappings}")
print(f"workflow_mappings长度: {len(self.workflow_mappings)}") print(f"workflow_mappings长度: {len(self.workflow_mappings)}")
self.in_temperature = 0.0
self.out_temperature = 0.0
self.pt100_temperature = 0.0
self.sensor_average_temperature = 0.0
self.target_temperature = 0.0
self.setting_temperature = 0.0
self.viscosity = 0.0
self.average_viscosity = 0.0
self.speed = 0.0
self.force = 0.0
self._frame_to_reactor_id = {1: "reactor_1", 2: "reactor_2", 3: "reactor_3", 4: "reactor_4", 5: "reactor_5"}
# ==================== 工作流方法 ==================== # ==================== 工作流方法 ====================
def reactor_taken_out(self): def reactor_taken_out(self):
@@ -291,22 +339,39 @@ class BioyondReactionStation(BioyondWorkstation):
def liquid_feeding_titration( def liquid_feeding_titration(
self, self,
volume_formula: str,
assign_material_name: str, assign_material_name: str,
titration_type: str = "1", volume_formula: str = None,
x_value: str = None,
feeding_order_data: str = None,
extracted_actuals: str = None,
titration_type: str = "2",
time: str = "90", time: str = "90",
torque_variation: int = 2, torque_variation: int = 2,
temperature: float = 25.00 temperature: float = 25.00
): ):
"""液体进料(滴定) """液体进料(滴定)
支持两种模式:
1. 直接提供 volume_formula (传统方式)
2. 自动计算公式: 提供 x_value, feeding_order_data, extracted_actuals (新方式)
Args: Args:
volume_formula: 分液公式(μL)
assign_material_name: 物料名称 assign_material_name: 物料名称
titration_type: 是否滴定(1=否, 2=是) volume_formula: 分液公式(μL),如果提供则直接使用,否则自动计算
x_value: 手工输入的x值,格式如 "1-2-3"
feeding_order_data: feeding_order JSON字符串或对象,用于获取m二酐值
extracted_actuals: 从报告提取的实际加料量JSON字符串,包含actualTargetWeigh和actualVolume
titration_type: 是否滴定(1=否, 2=是),默认2
time: 观察时间(分钟) time: 观察时间(分钟)
torque_variation: 是否观察(int类型, 1=否, 2=是) torque_variation: 是否观察(int类型, 1=否, 2=是)
temperature: 温度(°C) temperature: 温度(°C)
自动公式模板: 1000*(m二酐-x)*V二酐滴定/m二酐滴定
其中:
- m二酐滴定 = actualTargetWeigh (从extracted_actuals获取)
- V二酐滴定 = actualVolume (从extracted_actuals获取)
- x = x_value (手工输入)
- m二酐 = feeding_order中type为"main_anhydride"的amount值
""" """
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding(titration)"}') self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding(titration)"}')
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name) material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
@@ -316,6 +381,84 @@ class BioyondReactionStation(BioyondWorkstation):
if isinstance(temperature, str): if isinstance(temperature, str):
temperature = float(temperature) temperature = float(temperature)
# 如果没有直接提供volume_formula,则自动计算
if not volume_formula and x_value and feeding_order_data and extracted_actuals:
# 1. 解析 feeding_order_data 获取 m二酐
if isinstance(feeding_order_data, str):
try:
feeding_order_data = json.loads(feeding_order_data)
except json.JSONDecodeError as e:
raise ValueError(f"feeding_order_data JSON解析失败: {str(e)}")
# 支持两种格式:
# 格式1: 直接是数组 [{...}, {...}]
# 格式2: 对象包裹 {"feeding_order": [{...}, {...}]}
if isinstance(feeding_order_data, list):
feeding_order_list = feeding_order_data
elif isinstance(feeding_order_data, dict):
feeding_order_list = feeding_order_data.get("feeding_order", [])
else:
raise ValueError("feeding_order_data 必须是数组或包含feeding_order的字典")
# 从feeding_order中找到main_anhydride的amount
m_anhydride = None
for item in feeding_order_list:
if item.get("type") == "main_anhydride":
m_anhydride = item.get("amount")
break
if m_anhydride is None:
raise ValueError("在feeding_order中未找到type为'main_anhydride'的条目")
# 2. 解析 extracted_actuals 获取 actualTargetWeigh 和 actualVolume
if isinstance(extracted_actuals, str):
try:
extracted_actuals_obj = json.loads(extracted_actuals)
except json.JSONDecodeError as e:
raise ValueError(f"extracted_actuals JSON解析失败: {str(e)}")
else:
extracted_actuals_obj = extracted_actuals
# 获取actuals数组
actuals_list = extracted_actuals_obj.get("actuals", [])
if not actuals_list:
# actuals为空,无法自动生成公式,回退到手动模式
print(f"警告: extracted_actuals中actuals数组为空,无法自动生成公式,请手动提供volume_formula")
volume_formula = None # 清空,触发后续的错误检查
else:
# 根据assign_material_name匹配对应的actual数据
# 假设order_code中包含物料名称
matched_actual = None
for actual in actuals_list:
order_code = actual.get("order_code", "")
# 简单匹配:如果order_code包含物料名称
if assign_material_name in order_code:
matched_actual = actual
break
# 如果没有匹配到,使用第一个
if not matched_actual and actuals_list:
matched_actual = actuals_list[0]
if not matched_actual:
raise ValueError("无法从extracted_actuals中获取实际加料量数据")
m_anhydride_titration = matched_actual.get("actualTargetWeigh") # m二酐滴定
v_anhydride_titration = matched_actual.get("actualVolume") # V二酐滴定
if m_anhydride_titration is None or v_anhydride_titration is None:
raise ValueError(f"实际加料量数据不完整: actualTargetWeigh={m_anhydride_titration}, actualVolume={v_anhydride_titration}")
# 3. 构建公式: 1000*(m二酐-x)*V二酐滴定/m二酐滴定
# x_value 格式如 "{{1-2-3}}",保留完整格式(包括花括号)直接替换到公式中
volume_formula = f"1000*({m_anhydride}-{x_value})*{v_anhydride_titration}/{m_anhydride_titration}"
print(f"自动生成滴定公式: {volume_formula}")
print(f" m二酐={m_anhydride}, x={x_value}, V二酐滴定={v_anhydride_titration}, m二酐滴定={m_anhydride_titration}")
elif not volume_formula:
raise ValueError("必须提供 volume_formula 或 (x_value + feeding_order_data + extracted_actuals)")
liquid_step_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["liquid"] liquid_step_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["liquid"]
observe_step_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["observe"] observe_step_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["observe"]
@@ -343,6 +486,286 @@ class BioyondReactionStation(BioyondWorkstation):
print(f"当前队列长度: {len(self.pending_task_params)}") print(f"当前队列长度: {len(self.pending_task_params)}")
return json.dumps({"suc": True}) return json.dumps({"suc": True})
def _extract_actuals_from_report(self, report) -> Dict[str, Any]:
data = report.get('data') if isinstance(report, dict) else None
actual_target_weigh = None
actual_volume = None
if data:
extra = data.get('extraProperties') or {}
if isinstance(extra, dict):
for v in extra.values():
obj = None
try:
obj = json.loads(v) if isinstance(v, str) else v
except Exception:
obj = None
if isinstance(obj, dict):
tw = obj.get('targetWeigh')
vol = obj.get('volume')
if tw is not None:
try:
actual_target_weigh = float(tw)
except Exception:
pass
if vol is not None:
try:
actual_volume = float(vol)
except Exception:
pass
return {
'actualTargetWeigh': actual_target_weigh,
'actualVolume': actual_volume
}
def extract_actuals_from_batch_reports(self, batch_reports_result: str) -> dict:
print(f"[DEBUG] extract_actuals 收到原始数据: {batch_reports_result[:500]}...") # 打印前500字符
try:
obj = json.loads(batch_reports_result) if isinstance(batch_reports_result, str) else batch_reports_result
if isinstance(obj, dict) and "return_info" in obj:
inner = obj["return_info"]
obj = json.loads(inner) if isinstance(inner, str) else inner
reports = obj.get("reports", []) if isinstance(obj, dict) else []
print(f"[DEBUG] 解析后的 reports 数组长度: {len(reports)}")
except Exception as e:
print(f"[DEBUG] 解析异常: {e}")
reports = []
actuals = []
for i, r in enumerate(reports):
print(f"[DEBUG] 处理 report[{i}]: order_code={r.get('order_code')}, has_extracted={r.get('extracted') is not None}, has_report={r.get('report') is not None}")
order_code = r.get("order_code")
order_id = r.get("order_id")
ex = r.get("extracted")
if isinstance(ex, dict) and (ex.get("actualTargetWeigh") is not None or ex.get("actualVolume") is not None):
print(f"[DEBUG] 从 extracted 字段提取: actualTargetWeigh={ex.get('actualTargetWeigh')}, actualVolume={ex.get('actualVolume')}")
actuals.append({
"order_code": order_code,
"order_id": order_id,
"actualTargetWeigh": ex.get("actualTargetWeigh"),
"actualVolume": ex.get("actualVolume")
})
continue
report = r.get("report")
vals = self._extract_actuals_from_report(report) if report else {"actualTargetWeigh": None, "actualVolume": None}
print(f"[DEBUG] 从 report 字段提取: {vals}")
actuals.append({
"order_code": order_code,
"order_id": order_id,
**vals
})
print(f"[DEBUG] 最终提取的 actuals 数组长度: {len(actuals)}")
result = {
"return_info": json.dumps({"actuals": actuals}, ensure_ascii=False)
}
print(f"[DEBUG] 返回结果: {result}")
return result
def process_temperature_cutoff_report(self, report_request) -> Dict[str, Any]:
try:
data = report_request.data
def _f(v):
try:
return float(v)
except Exception:
return 0.0
self.target_temperature = _f(data.get("targetTemperature"))
self.setting_temperature = _f(data.get("settingTemperature"))
self.in_temperature = _f(data.get("inTemperature"))
self.out_temperature = _f(data.get("outTemperature"))
self.pt100_temperature = _f(data.get("pt100Temperature"))
self.sensor_average_temperature = _f(data.get("sensorAverageTemperature"))
self.speed = _f(data.get("speed"))
self.force = _f(data.get("force"))
self.viscosity = _f(data.get("viscosity"))
self.average_viscosity = _f(data.get("averageViscosity"))
try:
if hasattr(self, "_ros_node") and self._ros_node is not None:
props = [
"in_temperature","out_temperature","pt100_temperature","sensor_average_temperature",
"target_temperature","setting_temperature","viscosity","average_viscosity",
"speed","force"
]
for name in props:
pub = self._ros_node._property_publishers.get(name)
if pub:
pub.publish_property()
frame = data.get("frameCode")
reactor_id = None
try:
reactor_id = self._frame_to_reactor_id.get(int(frame))
except Exception:
reactor_id = None
if reactor_id and hasattr(self._ros_node, "sub_devices"):
child = self._ros_node.sub_devices.get(reactor_id)
if child and hasattr(child, "driver_instance"):
child.driver_instance.update_metrics(data)
pubs = getattr(child.ros_node_instance, "_property_publishers", {})
for name in props:
p = pubs.get(name)
if p:
p.publish_property()
except Exception:
pass
event = {
"frameCode": data.get("frameCode"),
"generateTime": data.get("generateTime"),
"targetTemperature": data.get("targetTemperature"),
"settingTemperature": data.get("settingTemperature"),
"inTemperature": data.get("inTemperature"),
"outTemperature": data.get("outTemperature"),
"pt100Temperature": data.get("pt100Temperature"),
"sensorAverageTemperature": data.get("sensorAverageTemperature"),
"speed": data.get("speed"),
"force": data.get("force"),
"viscosity": data.get("viscosity"),
"averageViscosity": data.get("averageViscosity"),
"request_time": report_request.request_time,
"timestamp": datetime.now().isoformat(),
"reactor_id": self._frame_to_reactor_id.get(int(data.get("frameCode", 0))) if str(data.get("frameCode", "")).isdigit() else None,
}
base_dir = Path(__file__).resolve().parents[3] / "unilabos_data"
base_dir.mkdir(parents=True, exist_ok=True)
out_file = base_dir / "temperature_cutoff_events.json"
try:
existing = json.loads(out_file.read_text(encoding="utf-8")) if out_file.exists() else []
if not isinstance(existing, list):
existing = []
except Exception:
existing = []
existing.append(event)
out_file.write_text(json.dumps(existing, ensure_ascii=False, indent=2), encoding="utf-8")
if hasattr(self, "_ros_node") and self._ros_node is not None:
ns = self._ros_node.namespace
topics = {
"targetTemperature": f"{ns}/metrics/temperature_cutoff/target_temperature",
"settingTemperature": f"{ns}/metrics/temperature_cutoff/setting_temperature",
"inTemperature": f"{ns}/metrics/temperature_cutoff/in_temperature",
"outTemperature": f"{ns}/metrics/temperature_cutoff/out_temperature",
"pt100Temperature": f"{ns}/metrics/temperature_cutoff/pt100_temperature",
"sensorAverageTemperature": f"{ns}/metrics/temperature_cutoff/sensor_average_temperature",
"speed": f"{ns}/metrics/temperature_cutoff/speed",
"force": f"{ns}/metrics/temperature_cutoff/force",
"viscosity": f"{ns}/metrics/temperature_cutoff/viscosity",
"averageViscosity": f"{ns}/metrics/temperature_cutoff/average_viscosity",
}
for k, t in topics.items():
v = data.get(k)
if v is not None:
pub = self._ros_node.create_publisher(Float64, t, 10)
pub.publish(convert_to_ros_msg(Float64, float(v)))
evt_pub = self._ros_node.create_publisher(String, f"{ns}/events/temperature_cutoff", 10)
evt_pub.publish(convert_to_ros_msg(String, json.dumps(event, ensure_ascii=False)))
return {"processed": True, "frame": data.get("frameCode")}
except Exception as e:
return {"processed": False, "error": str(e)}
def wait_for_multiple_orders_and_get_reports(self, batch_create_result: str = None, timeout: int = 7200, check_interval: int = 10) -> Dict[str, Any]:
try:
timeout = int(timeout) if timeout else 7200
check_interval = int(check_interval) if check_interval else 10
if not batch_create_result or batch_create_result == "":
raise ValueError("batch_create_result为空")
try:
if isinstance(batch_create_result, str) and '[...]' in batch_create_result:
batch_create_result = batch_create_result.replace('[...]', '[]')
result_obj = json.loads(batch_create_result) if isinstance(batch_create_result, str) else batch_create_result
if isinstance(result_obj, dict) and "return_value" in result_obj:
inner = result_obj.get("return_value")
if isinstance(inner, str):
result_obj = json.loads(inner)
elif isinstance(inner, dict):
result_obj = inner
order_codes = result_obj.get("order_codes", [])
order_ids = result_obj.get("order_ids", [])
except Exception as e:
raise ValueError(f"解析batch_create_result失败: {e}")
if not order_codes or not order_ids:
raise ValueError("缺少order_codes或order_ids")
if not isinstance(order_codes, list):
order_codes = [order_codes]
if not isinstance(order_ids, list):
order_ids = [order_ids]
if len(order_codes) != len(order_ids):
raise ValueError("order_codes与order_ids数量不匹配")
total = len(order_codes)
pending = {c: {"order_id": order_ids[i], "completed": False} for i, c in enumerate(order_codes)}
reports = []
start_time = time.time()
while pending:
elapsed_time = time.time() - start_time
if elapsed_time > timeout:
for oc in list(pending.keys()):
reports.append({
"order_code": oc,
"order_id": pending[oc]["order_id"],
"status": "timeout",
"completion_status": None,
"report": None,
"extracted": None,
"elapsed_time": elapsed_time
})
break
completed_round = []
for oc in list(pending.keys()):
oid = pending[oc]["order_id"]
if oc in self.order_completion_status:
info = self.order_completion_status[oc]
try:
rq = json.dumps({"order_id": oid})
rep = self.hardware_interface.order_report(rq)
if not rep:
rep = {"error": "无法获取报告"}
reports.append({
"order_code": oc,
"order_id": oid,
"status": "completed",
"completion_status": info.get('status'),
"report": rep,
"extracted": self._extract_actuals_from_report(rep),
"elapsed_time": elapsed_time
})
completed_round.append(oc)
del self.order_completion_status[oc]
except Exception as e:
reports.append({
"order_code": oc,
"order_id": oid,
"status": "error",
"completion_status": info.get('status') if 'info' in locals() else None,
"report": None,
"extracted": None,
"error": str(e),
"elapsed_time": elapsed_time
})
completed_round.append(oc)
for oc in completed_round:
del pending[oc]
if pending:
time.sleep(check_interval)
completed_count = sum(1 for r in reports if r['status'] == 'completed')
timeout_count = sum(1 for r in reports if r['status'] == 'timeout')
error_count = sum(1 for r in reports if r['status'] == 'error')
final_elapsed_time = time.time() - start_time
summary = {
"total": total,
"completed": completed_count,
"timeout": timeout_count,
"error": error_count,
"elapsed_time": round(final_elapsed_time, 2),
"reports": reports
}
return {
"return_info": json.dumps(summary, ensure_ascii=False)
}
except Exception as e:
raise
def liquid_feeding_beaker( def liquid_feeding_beaker(
self, self,
volume: str = "350", volume: str = "350",

View File

@@ -9,6 +9,7 @@ import traceback
from datetime import datetime from datetime import datetime
from typing import Dict, Any, List, Optional, Union from typing import Dict, Any, List, Optional, Union
import json import json
from pathlib import Path
from unilabos.devices.workstation.workstation_base import WorkstationBase, ResourceSynchronizer from unilabos.devices.workstation.workstation_base import WorkstationBase, ResourceSynchronizer
from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondV1RPC from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondV1RPC
@@ -19,6 +20,7 @@ from unilabos.resources.graphio import resource_bioyond_to_plr, resource_plr_to_
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, BaseROS2DeviceNode from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, BaseROS2DeviceNode
from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode
from unilabos.ros.msgs.message_converter import convert_to_ros_msg, Float64, String
from pylabrobot.resources.resource import Resource as ResourcePLR from pylabrobot.resources.resource import Resource as ResourcePLR
from unilabos.devices.workstation.bioyond_studio.config import ( from unilabos.devices.workstation.bioyond_studio.config import (
@@ -676,13 +678,13 @@ class BioyondWorkstation(WorkstationBase):
self._synced_resources = [] self._synced_resources = []
def transfer_resource_to_another(self, resource: List[ResourceSlot], mount_resource: List[ResourceSlot], sites: List[str], mount_device_id: DeviceSlot): def transfer_resource_to_another(self, resource: List[ResourceSlot], mount_resource: List[ResourceSlot], sites: List[str], mount_device_id: DeviceSlot):
time.sleep(3) future = ROS2DeviceNode.run_async_func(self._ros_node.transfer_resource_to_another, True, **{
ROS2DeviceNode.run_async_func(self._ros_node.transfer_resource_to_another, True, **{
"plr_resources": resource, "plr_resources": resource,
"target_device_id": mount_device_id, "target_device_id": mount_device_id,
"target_resources": mount_resource, "target_resources": mount_resource,
"sites": sites, "sites": sites,
}) })
return future
def _create_communication_module(self, config: Optional[Dict[str, Any]] = None) -> None: def _create_communication_module(self, config: Optional[Dict[str, Any]] = None) -> None:
"""创建Bioyond通信模块""" """创建Bioyond通信模块"""
@@ -1327,6 +1329,7 @@ class BioyondWorkstation(WorkstationBase):
logger.error(f"处理物料变更报送失败: {e}") logger.error(f"处理物料变更报送失败: {e}")
return {"processed": False, "error": str(e)} return {"processed": False, "error": str(e)}
def handle_external_error(self, error_data: Dict[str, Any]) -> Dict[str, Any]: def handle_external_error(self, error_data: Dict[str, Any]) -> Dict[str, Any]:
"""处理错误处理报送 """处理错误处理报送

View File

@@ -22,6 +22,7 @@ from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse from urllib.parse import urlparse
from dataclasses import dataclass, asdict from dataclasses import dataclass, asdict
from datetime import datetime from datetime import datetime
from pathlib import Path
from unilabos.utils.log import logger from unilabos.utils.log import logger
@@ -76,6 +77,14 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
logger.info(f"收到工作站报送: {endpoint} - {request_data.get('token', 'unknown')}") logger.info(f"收到工作站报送: {endpoint} - {request_data.get('token', 'unknown')}")
try:
payload_for_log = {"method": "POST", **request_data}
self._save_raw_request(endpoint, payload_for_log)
if hasattr(self.workstation, '_reports_received_count'):
self.workstation._reports_received_count += 1
except Exception:
pass
# 统一的报送端点路由基于LIMS协议规范 # 统一的报送端点路由基于LIMS协议规范
if endpoint == '/report/step_finish': if endpoint == '/report/step_finish':
response = self._handle_step_finish_report(request_data) response = self._handle_step_finish_report(request_data)
@@ -90,6 +99,8 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
response = self._handle_material_change_report(request_data) response = self._handle_material_change_report(request_data)
elif endpoint == '/report/error_handling': elif endpoint == '/report/error_handling':
response = self._handle_error_handling_report(request_data) response = self._handle_error_handling_report(request_data)
elif endpoint == '/report/temperature-cutoff':
response = self._handle_temperature_cutoff_report(request_data)
# 保留LIMS协议端点以兼容现有系统 # 保留LIMS协议端点以兼容现有系统
elif endpoint == '/LIMS/step_finish': elif endpoint == '/LIMS/step_finish':
response = self._handle_step_finish_report(request_data) response = self._handle_step_finish_report(request_data)
@@ -107,7 +118,8 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
"/report/order_finish", "/report/order_finish",
"/report/batch_update", "/report/batch_update",
"/report/material_change", "/report/material_change",
"/report/error_handling" "/report/error_handling",
"/report/temperature-cutoff"
]} ]}
) )
@@ -128,6 +140,11 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
parsed_path = urlparse(self.path) parsed_path = urlparse(self.path)
endpoint = parsed_path.path endpoint = parsed_path.path
try:
self._save_raw_request(endpoint, {"method": "GET"})
except Exception:
pass
if endpoint == '/status': if endpoint == '/status':
response = self._handle_status_check() response = self._handle_status_check()
elif endpoint == '/health': elif endpoint == '/health':
@@ -318,6 +335,50 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
message=f"任务完成报送处理失败: {str(e)}" message=f"任务完成报送处理失败: {str(e)}"
) )
def _handle_temperature_cutoff_report(self, request_data: Dict[str, Any]) -> HttpResponse:
try:
required_fields = ['token', 'request_time', 'data']
if missing := [f for f in required_fields if f not in request_data]:
return HttpResponse(success=False, message=f"缺少必要字段: {', '.join(missing)}")
data = request_data['data']
metrics = [
'frameCode',
'generateTime',
'targetTemperature',
'settingTemperature',
'inTemperature',
'outTemperature',
'pt100Temperature',
'sensorAverageTemperature',
'speed',
'force',
'viscosity',
'averageViscosity'
]
if miss := [f for f in metrics if f not in data]:
return HttpResponse(success=False, message=f"data字段缺少必要内容: {', '.join(miss)}")
report_request = WorkstationReportRequest(
token=request_data['token'],
request_time=request_data['request_time'],
data=data
)
result = {}
if hasattr(self.workstation, 'process_temperature_cutoff_report'):
result = self.workstation.process_temperature_cutoff_report(report_request)
return HttpResponse(
success=True,
message=f"温度/粘度报送已处理: 帧{data['frameCode']}",
acknowledgment_id=f"TEMP_CUTOFF_{int(time.time()*1000)}_{data['frameCode']}",
data=result
)
except Exception as e:
logger.error(f"处理温度/粘度报送失败: {e}\n{traceback.format_exc()}")
return HttpResponse(success=False, message=f"温度/粘度报送处理失败: {str(e)}")
def _handle_batch_update_report(self, request_data: Dict[str, Any]) -> HttpResponse: def _handle_batch_update_report(self, request_data: Dict[str, Any]) -> HttpResponse:
"""处理批量报送""" """处理批量报送"""
try: try:
@@ -510,6 +571,7 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
"POST /report/batch_update", "POST /report/batch_update",
"POST /report/material_change", "POST /report/material_change",
"POST /report/error_handling", "POST /report/error_handling",
"POST /report/temperature-cutoff",
"GET /status", "GET /status",
"GET /health" "GET /health"
] ]
@@ -547,6 +609,22 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
"""重写日志方法""" """重写日志方法"""
logger.debug(f"HTTP请求: {format % args}") logger.debug(f"HTTP请求: {format % args}")
def _save_raw_request(self, endpoint: str, request_data: Dict[str, Any]) -> None:
try:
base_dir = Path(__file__).resolve().parents[3] / "unilabos_data" / "http_reports"
base_dir.mkdir(parents=True, exist_ok=True)
log_path = getattr(self.workstation, "_http_log_path", None)
log_file = Path(log_path) if log_path else (base_dir / f"http_{int(time.time()*1000)}.log")
payload = {
"endpoint": endpoint,
"received_at": datetime.now().isoformat(),
"body": request_data
}
with open(log_file, "a", encoding="utf-8") as f:
f.write(json.dumps(payload, ensure_ascii=False) + "\n")
except Exception:
pass
class WorkstationHTTPService: class WorkstationHTTPService:
"""工作站HTTP服务""" """工作站HTTP服务"""
@@ -572,6 +650,10 @@ class WorkstationHTTPService:
# 创建HTTP服务器 # 创建HTTP服务器
self.server = HTTPServer((self.host, self.port), handler_factory) self.server = HTTPServer((self.host, self.port), handler_factory)
base_dir = Path(__file__).resolve().parents[3] / "unilabos_data" / "http_reports"
base_dir.mkdir(parents=True, exist_ok=True)
session_log = base_dir / f"http_{int(time.time()*1000)}.log"
setattr(self.workstation, "_http_log_path", str(session_log))
# 安全地获取 device_id 用于线程命名 # 安全地获取 device_id 用于线程命名
device_id = "unknown" device_id = "unknown"
@@ -599,6 +681,7 @@ class WorkstationHTTPService:
logger.info("扩展报送端点:") logger.info("扩展报送端点:")
logger.info(" - POST /report/material_change # 物料变更报送") logger.info(" - POST /report/material_change # 物料变更报送")
logger.info(" - POST /report/error_handling # 错误处理报送") logger.info(" - POST /report/error_handling # 错误处理报送")
logger.info(" - POST /report/temperature-cutoff # 温度/粘度报送")
logger.info("兼容端点:") logger.info("兼容端点:")
logger.info(" - POST /LIMS/step_finish # 兼容LIMS步骤完成") logger.info(" - POST /LIMS/step_finish # 兼容LIMS步骤完成")
logger.info(" - POST /LIMS/preintake_finish # 兼容LIMS通量完成") logger.info(" - POST /LIMS/preintake_finish # 兼容LIMS通量完成")
@@ -700,6 +783,9 @@ if __name__ == "__main__":
def handle_external_error(self, error_data): def handle_external_error(self, error_data):
return {"handled": True} return {"handled": True}
def process_temperature_cutoff_report(self, report_request):
return {"processed": True, "metrics": report_request.data}
workstation = BioyondWorkstation() workstation = BioyondWorkstation()
http_service = WorkstationHTTPService(workstation) http_service = WorkstationHTTPService(workstation)
@@ -723,4 +809,3 @@ if __name__ == "__main__":
except Exception as e: except Exception as e:
print(f"服务器运行错误: {e}") print(f"服务器运行错误: {e}")
http_service.stop() http_service.stop()

View File

@@ -1,4 +1,4 @@
camera.USB: camera:
category: category:
- camera - camera
class: class:

View File

@@ -4,106 +4,6 @@ reaction_station.bioyond:
- reaction_station_bioyond - reaction_station_bioyond
class: class:
action_value_mappings: action_value_mappings:
auto-create_order:
feedback: {}
goal: {}
goal_default:
json_str: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
json_str:
type: string
required:
- json_str
type: object
result: {}
required:
- goal
title: create_order参数
type: object
type: UniLabJsonCommand
auto-merge_workflow_with_parameters:
feedback: {}
goal: {}
goal_default:
json_str: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
json_str:
type: string
required:
- json_str
type: object
result: {}
required:
- goal
title: merge_workflow_with_parameters参数
type: object
type: UniLabJsonCommand
auto-process_web_workflows:
feedback: {}
goal: {}
goal_default:
web_workflow_json: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
web_workflow_json:
type: string
required:
- web_workflow_json
type: object
result: {}
required:
- goal
title: process_web_workflows参数
type: object
type: UniLabJsonCommand
auto-workflow_step_query:
feedback: {}
goal: {}
goal_default:
workflow_id: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
workflow_id:
type: string
required:
- workflow_id
type: object
result: {}
required:
- goal
title: workflow_step_query参数
type: object
type: UniLabJsonCommand
drip_back: drip_back:
feedback: {} feedback: {}
goal: goal:
@@ -160,6 +60,56 @@ reaction_station.bioyond:
title: drip_back参数 title: drip_back参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
extract_actuals_from_batch_reports:
feedback: {}
goal:
batch_reports_result: batch_reports_result
goal_default:
batch_reports_result: ''
handles:
input:
- data_key: batch_reports_result
data_source: handle
data_type: string
handler_key: BATCH_REPORTS_RESULT
io_type: source
label: Batch Order Completion Reports
output:
- data_key: return_info
data_source: executor
data_type: string
handler_key: ACTUALS_EXTRACTED
io_type: sink
label: Extracted Actuals
result:
return_info: return_info
schema:
description: 从批量任务完成报告中提取每个订单的实际加料量输出extracted列表。
properties:
feedback: {}
goal:
properties:
batch_reports_result:
description: 批量任务完成信息JSON字符串或对象包含reports数组
type: string
required:
- batch_reports_result
type: object
result:
properties:
return_info:
description: JSON字符串包含actuals数组每项含order_code, order_id, actualTargetWeigh,
actualVolume
type: string
required:
- return_info
title: extract_actuals_from_batch_reports结果
type: object
required:
- goal
title: extract_actuals_from_batch_reports参数
type: object
type: UniLabJsonCommand
liquid_feeding_beaker: liquid_feeding_beaker:
feedback: {} feedback: {}
goal: goal:
@@ -287,22 +237,41 @@ reaction_station.bioyond:
feedback: {} feedback: {}
goal: goal:
assign_material_name: assign_material_name assign_material_name: assign_material_name
extracted_actuals: extracted_actuals
feeding_order_data: feeding_order_data
temperature: temperature temperature: temperature
time: time time: time
titration_type: titration_type titration_type: titration_type
torque_variation: torque_variation torque_variation: torque_variation
volume_formula: volume_formula volume_formula: volume_formula
x_value: x_value
goal_default: goal_default:
assign_material_name: '' assign_material_name: ''
temperature: '' extracted_actuals: ''
time: '' feeding_order_data: ''
titration_type: '' temperature: '25.00'
torque_variation: '' time: '90'
titration_type: '2'
torque_variation: '2'
volume_formula: '' volume_formula: ''
handles: {} x_value: ''
handles:
input:
- data_key: extracted_actuals
data_source: handle
data_type: string
handler_key: ACTUALS_EXTRACTED
io_type: source
label: Extracted Actuals From Reports
- data_key: feeding_order_data
data_source: handle
data_type: object
handler_key: feeding_order
io_type: source
label: Feeding Order Data From Calculation Node
result: {} result: {}
schema: schema:
description: 液体进料(滴定) description: 液体进料(滴定)。支持两种模式:1)直接提供volume_formula;2)自动计算-提供x_value+feeding_order_data+extracted_actuals,系统自动生成公式"1000*(m二酐-x)*V二酐滴定/m二酐滴定"
properties: properties:
feedback: {} feedback: {}
goal: goal:
@@ -310,28 +279,37 @@ reaction_station.bioyond:
assign_material_name: assign_material_name:
description: 物料名称 description: 物料名称
type: string type: string
extracted_actuals:
description: 从报告提取的实际加料量JSON字符串,包含actualTargetWeigh(m二酐滴定)和actualVolume(V二酐滴定)
type: string
feeding_order_data:
description: 'feeding_order JSON对象,用于获取m二酐值(type为main_anhydride的amount)。示例:
{"feeding_order": [{"type": "main_anhydride", "amount": 1.915}]}'
type: string
temperature: temperature:
description: 温度设定(°C) default: '25.00'
description: 温度设定(°C),默认25.00
type: string type: string
time: time:
description: 观察时间(分钟) default: '90'
description: 观察时间(分钟),默认90
type: string type: string
titration_type: titration_type:
description: 是否滴定(1=否, 2=是) default: '2'
description: 是否滴定(1=否, 2=是),默认2
type: string type: string
torque_variation: torque_variation:
description: 是否观察 (1=否, 2=是) default: '2'
description: 是否观察 (1=否, 2=是),默认2
type: string type: string
volume_formula: volume_formula:
description: 分液公式(μL) description: 分液公式(μL)。可直接提供固定公式,或留空由系统根据x_value、feeding_order_data、extracted_actuals自动生成
type: string
x_value:
description: 公式中的x值,手工输入,格式为"{{1-2-3}}"(包含双花括号)。用于自动公式计算
type: string type: string
required: required:
- volume_formula
- assign_material_name - assign_material_name
- time
- torque_variation
- titration_type
- temperature
type: object type: object
result: {} result: {}
required: required:
@@ -546,7 +524,19 @@ reaction_station.bioyond:
module: unilabos.devices.workstation.bioyond_studio.reaction_station:BioyondReactionStation module: unilabos.devices.workstation.bioyond_studio.reaction_station:BioyondReactionStation
protocol_type: [] protocol_type: []
status_types: status_types:
workflow_sequence: String all_workflows: dict
average_viscosity: float
bioyond_status: dict
force: float
in_temperature: float
out_temperature: float
pt100_temperature: float
sensor_average_temperature: float
setting_temperature: float
speed: float
target_temperature: float
viscosity: float
workstation_status: dict
type: python type: python
config_info: [] config_info: []
description: Bioyond反应站 description: Bioyond反应站
@@ -558,18 +548,75 @@ reaction_station.bioyond:
config: config:
type: object type: object
deck: deck:
type: string type: object
protocol_type:
type: string
required: [] required: []
type: object type: object
data: data:
properties: properties:
workflow_sequence: all_workflows:
items: type: object
type: string bioyond_status:
type: array type: object
workstation_status:
type: object
required: required:
- workflow_sequence - bioyond_status
- all_workflows
- workstation_status
type: object
version: 1.0.0
reaction_station.reactor:
category:
- reactor
- reaction_station_bioyond
class:
action_value_mappings: {}
module: unilabos.devices.workstation.bioyond_studio.reaction_station:BioyondReactor
status_types:
average_viscosity: float
force: float
in_temperature: float
out_temperature: float
pt100_temperature: float
sensor_average_temperature: float
setting_temperature: float
speed: float
target_temperature: float
viscosity: float
type: python
config_info: []
description: 反应站子设备-反应器
handles: []
icon: reaction_station.webp
init_param_schema:
config:
properties:
config:
type: object
required: []
type: object
data:
properties:
average_viscosity:
type: number
force:
type: number
in_temperature:
type: number
out_temperature:
type: number
pt100_temperature:
type: number
sensor_average_temperature:
type: number
setting_temperature:
type: number
speed:
type: number
target_temperature:
type: number
viscosity:
type: number
required: []
type: object type: object
version: 1.0.0 version: 1.0.0

View File

@@ -18,9 +18,9 @@ def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
dx=10.0, dx=10.0,
dy=10.0, dy=10.0,
dz=10.0, dz=10.0,
item_dx=137.0, item_dx=147.0,
item_dy=96.0, item_dy=106.0,
item_dz=120.0, item_dz=130.0,
category="warehouse", category="warehouse",
col_offset=0, # 从01开始: A01, A02, A03, A04 col_offset=0, # 从01开始: A01, A02, A03, A04
layout="row-major", # ⭐ 改为行优先排序 layout="row-major", # ⭐ 改为行优先排序

View File

@@ -6,12 +6,13 @@ from cv_bridge import CvBridge
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, DeviceNodeResourceTracker from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, DeviceNodeResourceTracker
class VideoPublisher(BaseROS2DeviceNode): class VideoPublisher(BaseROS2DeviceNode):
def __init__(self, device_id='video_publisher', camera_index=0, period: float = 0.1, resource_tracker: DeviceNodeResourceTracker = None): def __init__(self, device_id='video_publisher', device_uuid='', camera_index=0, period: float = 0.1, resource_tracker: DeviceNodeResourceTracker = None):
# 初始化BaseROS2DeviceNode使用自身作为driver_instance # 初始化BaseROS2DeviceNode使用自身作为driver_instance
BaseROS2DeviceNode.__init__( BaseROS2DeviceNode.__init__(
self, self,
driver_instance=self, driver_instance=self,
device_id=device_id, device_id=device_id,
device_uuid=device_uuid,
status_types={}, status_types={},
action_value_mappings={}, action_value_mappings={},
hardware_interface="camera", hardware_interface="camera",

View File

View File

@@ -5,10 +5,16 @@
"name": "reaction_station_bioyond", "name": "reaction_station_bioyond",
"parent": null, "parent": null,
"children": [ "children": [
"Bioyond_Deck" "Bioyond_Deck",
"reactor_1",
"reactor_2",
"reactor_3",
"reactor_4",
"reactor_5"
], ],
"type": "device", "type": "device",
"class": "reaction_station.bioyond", "class": "reaction_station.bioyond",
"position": {"x": 0, "y": 3800, "z": 0},
"config": { "config": {
"config": { "config": {
"api_key": "DE9BDDA0", "api_key": "DE9BDDA0",
@@ -64,6 +70,61 @@
}, },
"data": {} "data": {}
}, },
{
"id": "reactor_1",
"name": "reactor_1",
"children": [],
"parent": "reaction_station_bioyond",
"type": "device",
"class": "reaction_station.reactor",
"position": {"x": 1150, "y": 380, "z": 0},
"config": {},
"data": {}
},
{
"id": "reactor_2",
"name": "reactor_2",
"children": [],
"parent": "reaction_station_bioyond",
"type": "device",
"class": "reaction_station.reactor",
"position": {"x": 1365, "y": 380, "z": 0},
"config": {},
"data": {}
},
{
"id": "reactor_3",
"name": "reactor_3",
"children": [],
"parent": "reaction_station_bioyond",
"type": "device",
"class": "reaction_station.reactor",
"position": {"x": 1580, "y": 380, "z": 0},
"config": {},
"data": {}
},
{
"id": "reactor_4",
"name": "reactor_4",
"children": [],
"parent": "reaction_station_bioyond",
"type": "device",
"class": "reaction_station.reactor",
"position": {"x": 1790, "y": 380, "z": 0},
"config": {},
"data": {}
},
{
"id": "reactor_5",
"name": "reactor_5",
"children": [],
"parent": "reaction_station_bioyond",
"type": "device",
"class": "reaction_station.reactor",
"position": {"x": 2010, "y": 380, "z": 0},
"config": {},
"data": {}
},
{ {
"id": "Bioyond_Deck", "id": "Bioyond_Deck",
"name": "Bioyond_Deck", "name": "Bioyond_Deck",

Some files were not shown because too many files have changed in this diff Show More