mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-17 04:51:10 +00:00
update docs, test examples
fix liquid_handler init bug
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,18 @@
|
||||
# 添加新设备
|
||||
# 添加设备:编写驱动
|
||||
|
||||
在 Uni-Lab 中,设备(Device)是实验操作的基础单元。Uni-Lab 使用**注册表机制**来兼容管理种类繁多的设备驱动程序。回顾 {ref}`instructions` 中的概念,抽象的设备对外拥有【话题】【服务】【动作】三种通信机制,因此将设备添加进 Uni-Lab,实际上是将设备驱动中的三种机制映射到 Uni-Lab 标准指令集上。
|
||||
在 Uni-Lab 中,设备(Device)是实验操作的基础单元。Uni-Lab 使用**注册表机制**来兼容管理种类繁多的设备驱动程序。抽象的设备对外拥有【话题】【服务】【动作】三种通信机制,因此将设备添加进 Uni-Lab,实际上是将设备驱动中的这三种机制映射到 Uni-Lab 标准指令集上。
|
||||
|
||||
能被 Uni-Lab 添加的驱动程序类型有以下种类:
|
||||
> **💡 提示:** 本文档介绍如何使用已有的设备驱动(SDK)。若设备没有现成的驱动程序,需要自己开发驱动,请参考 {doc}`add_old_device`。
|
||||
|
||||
1. Python Class,如
|
||||
## 支持的驱动类型
|
||||
|
||||
Uni-Lab 支持以下两种驱动程序:
|
||||
|
||||
### 1. Python Class(推荐)
|
||||
|
||||
Python 类设备驱动在完成注册表后可以直接在 Uni-Lab 中使用,无需额外编译。
|
||||
|
||||
**示例:**
|
||||
|
||||
```python
|
||||
class MockGripper:
|
||||
@@ -31,12 +39,11 @@ class MockGripper:
|
||||
def status(self) -> str:
|
||||
return self._status
|
||||
|
||||
# 会被自动识别的设备动作,接入 Uni-Lab 时会作为 ActionServer 接受任意控制者的指令
|
||||
@status.setter
|
||||
def status(self, target):
|
||||
self._status = target
|
||||
|
||||
# 需要在注册表添加的设备动作,接入 Uni-Lab 时会作为 ActionServer 接受任意控制者的指令
|
||||
# 会被自动识别的设备动作,接入 Uni-Lab 时会作为 ActionServer 接受任意控制者的指令
|
||||
def push_to(self, position: float, torque: float, velocity: float = 0.0):
|
||||
self._status = "Running"
|
||||
current_pos = self.position
|
||||
@@ -53,9 +60,11 @@ class MockGripper:
|
||||
self._status = "Idle"
|
||||
```
|
||||
|
||||
Python 类设备驱动在完成注册表后可以直接在 Uni-Lab 使用。
|
||||
### 2. C# Class
|
||||
|
||||
2. C# Class,如
|
||||
C# 驱动设备在完成注册表后,需要调用 Uni-Lab C# 编译后才能使用(仅需一次)。
|
||||
|
||||
**示例:**
|
||||
|
||||
```csharp
|
||||
using System;
|
||||
@@ -84,7 +93,7 @@ public class MockGripper
|
||||
position = currentPos + (Position - currentPos) / 20 * (i + 1);
|
||||
torque = Torque / (20 - i);
|
||||
velocity = Velocity;
|
||||
await Task.Delay((int)(moveTime * 1000 / 20)); // Convert seconds to milliseconds
|
||||
await Task.Delay((int)(moveTime * 1000 / 20));
|
||||
}
|
||||
torque = Torque;
|
||||
status = "Idle";
|
||||
@@ -92,12 +101,16 @@ public class MockGripper
|
||||
}
|
||||
```
|
||||
|
||||
C# 驱动设备在完成注册表后,需要调用 Uni-Lab C# 编译后才能使用,但只需一次。
|
||||
---
|
||||
|
||||
## 快速开始:使用注册表编辑器(推荐)
|
||||
## 快速开始:两种方式添加设备
|
||||
|
||||
### 方式 1:使用注册表编辑器(推荐)
|
||||
|
||||
推荐使用 Uni-Lab-OS 自带的可视化编辑器,它能自动分析您的设备驱动并生成大部分配置:
|
||||
|
||||
**步骤:**
|
||||
|
||||
1. 启动 Uni-Lab-OS
|
||||
2. 在浏览器中打开"注册表编辑器"页面
|
||||
3. 选择您的 Python 设备驱动文件
|
||||
@@ -106,13 +119,18 @@ C# 驱动设备在完成注册表后,需要调用 Uni-Lab C# 编译后才能
|
||||
6. 点击"生成注册表",复制生成的内容
|
||||
7. 保存到 `devices/` 目录下
|
||||
|
||||
---
|
||||
**优点:**
|
||||
|
||||
## 手动编写注册表(简化版)
|
||||
- 自动识别设备属性和方法
|
||||
- 可视化界面,易于操作
|
||||
- 自动生成完整配置
|
||||
- 减少手动配置错误
|
||||
|
||||
### 方式 2:手动编写注册表(简化版)
|
||||
|
||||
如果需要手动编写,只需要提供两个必需字段,系统会自动补全其余内容:
|
||||
|
||||
### 最小配置示例
|
||||
**最小配置示例:**
|
||||
|
||||
```yaml
|
||||
my_device: # 设备唯一标识符
|
||||
@@ -121,22 +139,22 @@ my_device: # 设备唯一标识符
|
||||
type: python # 驱动类型
|
||||
```
|
||||
|
||||
### 注册表文件位置
|
||||
**注册表文件位置:**
|
||||
|
||||
- 默认路径:`unilabos/registry/devices`
|
||||
- 自定义路径:启动时使用 `--registry` 参数指定
|
||||
- 可将多个设备写在同一个 yaml 文件中
|
||||
- 自定义路径:启动时使用 `--registry_path` 参数指定
|
||||
- 可将多个设备写在同一个 YAML 文件中
|
||||
|
||||
### 系统自动生成的内容
|
||||
**系统自动生成的内容:**
|
||||
|
||||
系统会自动分析您的 Python 驱动类并生成:
|
||||
|
||||
- `status_types`:从 `get_*` 方法自动识别状态属性
|
||||
- `status_types`:从 `@property` 装饰的方法自动识别状态属性
|
||||
- `action_value_mappings`:从类方法自动生成动作映射
|
||||
- `init_param_schema`:从 `__init__` 方法分析初始化参数
|
||||
- `schema`:前端显示用的属性类型定义
|
||||
|
||||
### 完整结构概览
|
||||
**完整结构概览:**
|
||||
|
||||
```yaml
|
||||
my_device:
|
||||
@@ -151,4 +169,848 @@ my_device:
|
||||
schema: {} # 自动生成
|
||||
```
|
||||
|
||||
详细的注册表编写指南和高级配置,请参考{doc}`yaml 注册表编写指南 <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.sleep(ROS2 异步机制)
|
||||
await self.sleep(duration)
|
||||
return {"success": True}
|
||||
```
|
||||
|
||||
**特点**:
|
||||
|
||||
- 普通方法或 async 方法
|
||||
- 返回 Dict 类型的结果
|
||||
- 自动注册为 ROS2 Action
|
||||
- 支持参数和返回值
|
||||
|
||||
### 返回值设计指南
|
||||
|
||||
> **⚠️ 重要:返回值会自动显示在前端**
|
||||
>
|
||||
> 动作方法的返回值(字典)会自动显示在 Web 界面的工作流执行结果中。因此,**强烈建议**设计结构化、可读的返回值字典。
|
||||
|
||||
**推荐的返回值结构:**
|
||||
|
||||
```python
|
||||
def my_action(self, param: float) -> Dict[str, Any]:
|
||||
"""执行操作"""
|
||||
try:
|
||||
# 执行操作...
|
||||
result = self._do_something(param)
|
||||
|
||||
return {
|
||||
"success": True, # 必需:操作是否成功
|
||||
"message": "操作完成", # 推荐:用户友好的消息
|
||||
"result": result, # 可选:具体结果数据
|
||||
"param_used": param, # 可选:记录使用的参数
|
||||
# 其他有用的信息...
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"message": "操作失败"
|
||||
}
|
||||
```
|
||||
|
||||
**最佳实践示例(参考 `host_node.test_latency`):**
|
||||
|
||||
```python
|
||||
def test_latency(self) -> Dict[str, Any]:
|
||||
"""测试网络延迟
|
||||
|
||||
返回值会在前端显示,包含详细的测试结果
|
||||
"""
|
||||
# 执行测试...
|
||||
avg_rtt_ms = 25.5
|
||||
avg_time_diff_ms = 10.2
|
||||
test_count = 5
|
||||
|
||||
# 返回结构化的测试结果
|
||||
return {
|
||||
"status": "success", # 状态标识
|
||||
"avg_rtt_ms": avg_rtt_ms, # 平均往返时间
|
||||
"avg_time_diff_ms": avg_time_diff_ms, # 平均时间差
|
||||
"max_time_error_ms": 5.3, # 最大误差
|
||||
"task_delay_ms": 15.7, # 任务延迟
|
||||
"test_count": test_count, # 测试次数
|
||||
}
|
||||
```
|
||||
|
||||
**前端显示效果:**
|
||||
|
||||
当用户在 Web 界面执行工作流时,返回的字典会以 JSON 格式显示在结果面板中:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"avg_rtt_ms": 25.5,
|
||||
"avg_time_diff_ms": 10.2,
|
||||
"max_time_error_ms": 5.3,
|
||||
"task_delay_ms": 15.7,
|
||||
"test_count": 5
|
||||
}
|
||||
```
|
||||
|
||||
**返回值设计建议:**
|
||||
|
||||
1. **始终包含 `success` 字段**:布尔值,表示操作是否成功
|
||||
2. **包含 `message` 字段**:字符串,提供用户友好的描述
|
||||
3. **使用有意义的键名**:使用描述性的键名(如 `avg_rtt_ms` 而不是 `v1`)
|
||||
4. **包含单位**:在键名中包含单位(如 `_ms`、`_ml`、`_celsius`)
|
||||
5. **记录重要参数**:返回使用的关键参数值,便于追溯
|
||||
6. **错误信息详细**:失败时包含 `error` 字段和详细的错误描述
|
||||
7. **避免返回大数据**:不要返回大型数组或二进制数据,这会影响前端性能
|
||||
|
||||
**错误处理示例:**
|
||||
|
||||
```python
|
||||
def risky_operation(self, param: float) -> Dict[str, Any]:
|
||||
"""可能失败的操作"""
|
||||
if param < 0:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "参数不能为负数",
|
||||
"message": f"无效参数: {param}",
|
||||
"param": param
|
||||
}
|
||||
|
||||
try:
|
||||
result = self._execute(param)
|
||||
return {
|
||||
"success": True,
|
||||
"message": "操作成功",
|
||||
"result": result,
|
||||
"param": param
|
||||
}
|
||||
except IOError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "通信错误",
|
||||
"message": str(e),
|
||||
"device_status": self._status
|
||||
}
|
||||
```
|
||||
|
||||
## 特殊参数类型:ResourceSlot 和 DeviceSlot
|
||||
|
||||
Uni-Lab 提供特殊的参数类型,用于在方法中声明需要选择资源或设备。
|
||||
|
||||
### 导入类型
|
||||
|
||||
```python
|
||||
from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot
|
||||
from typing import List
|
||||
```
|
||||
|
||||
### ResourceSlot - 资源选择
|
||||
|
||||
用于需要选择物料资源的场景:
|
||||
|
||||
```python
|
||||
def pipette_liquid(
|
||||
self,
|
||||
source: ResourceSlot, # 单个源容器
|
||||
target: ResourceSlot, # 单个目标容器
|
||||
volume: float
|
||||
) -> Dict[str, Any]:
|
||||
"""从源容器吸取液体到目标容器
|
||||
|
||||
Args:
|
||||
source: 源容器(前端会显示资源选择下拉框)
|
||||
target: 目标容器(前端会显示资源选择下拉框)
|
||||
volume: 体积(μL)
|
||||
"""
|
||||
print(f"Pipetting {volume}μL from {source.id} to {target.id}")
|
||||
return {"success": True}
|
||||
```
|
||||
|
||||
**多选示例**:
|
||||
|
||||
```python
|
||||
def mix_multiple(
|
||||
self,
|
||||
containers: List[ResourceSlot], # 多个容器选择
|
||||
speed: float
|
||||
) -> Dict[str, Any]:
|
||||
"""混合多个容器
|
||||
|
||||
Args:
|
||||
containers: 容器列表(前端会显示多选下拉框)
|
||||
speed: 混合速度
|
||||
"""
|
||||
for container in containers:
|
||||
print(f"Mixing {container.name}")
|
||||
return {"success": True}
|
||||
```
|
||||
|
||||
### DeviceSlot - 设备选择
|
||||
|
||||
用于需要选择其他设备的场景:
|
||||
|
||||
```python
|
||||
def coordinate_with_device(
|
||||
self,
|
||||
other_device: DeviceSlot, # 单个设备选择
|
||||
command: str
|
||||
) -> Dict[str, Any]:
|
||||
"""与另一个设备协同工作
|
||||
|
||||
Args:
|
||||
other_device: 协同设备(前端会显示设备选择下拉框)
|
||||
command: 命令
|
||||
"""
|
||||
print(f"Coordinating with {other_device.name}")
|
||||
return {"success": True}
|
||||
```
|
||||
|
||||
**多设备示例**:
|
||||
|
||||
```python
|
||||
def sync_devices(
|
||||
self,
|
||||
devices: List[DeviceSlot], # 多个设备选择
|
||||
sync_signal: str
|
||||
) -> Dict[str, Any]:
|
||||
"""同步多个设备
|
||||
|
||||
Args:
|
||||
devices: 设备列表(前端会显示多选下拉框)
|
||||
sync_signal: 同步信号
|
||||
"""
|
||||
for dev in devices:
|
||||
print(f"Syncing {dev.name}")
|
||||
return {"success": True}
|
||||
```
|
||||
|
||||
### 完整示例:液体处理工作站
|
||||
|
||||
```python
|
||||
from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot
|
||||
from typing import List, Dict, Any
|
||||
|
||||
class LiquidHandler:
|
||||
"""液体处理工作站"""
|
||||
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
self.simulation = config.get('simulation', False)
|
||||
self._status = "idle"
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self._status
|
||||
|
||||
def transfer_liquid(
|
||||
self,
|
||||
source: ResourceSlot, # 源容器选择
|
||||
target: ResourceSlot, # 目标容器选择
|
||||
volume: float,
|
||||
tip: ResourceSlot = None # 可选的枪头选择
|
||||
) -> Dict[str, Any]:
|
||||
"""转移液体
|
||||
|
||||
前端效果:
|
||||
- source: 下拉框,列出所有可用容器
|
||||
- target: 下拉框,列出所有可用容器
|
||||
- volume: 数字输入框
|
||||
- tip: 下拉框(可选),列出所有枪头
|
||||
"""
|
||||
self._status = "transferring"
|
||||
|
||||
# source和target会被解析为实际的资源对象
|
||||
print(f"Transferring {volume}μL")
|
||||
print(f" From: {source.id} ({source.name})")
|
||||
print(f" To: {target.id} ({target.name})")
|
||||
|
||||
if tip:
|
||||
print(f" Using tip: {tip.id}")
|
||||
|
||||
# 执行实际的液体转移
|
||||
# ...
|
||||
|
||||
self._status = "idle"
|
||||
return {
|
||||
"success": True,
|
||||
"volume_transferred": volume,
|
||||
"source_id": source.id,
|
||||
"target_id": target.id
|
||||
}
|
||||
|
||||
def multi_dispense(
|
||||
self,
|
||||
source: ResourceSlot, # 单个源
|
||||
targets: List[ResourceSlot], # 多个目标
|
||||
volumes: List[float]
|
||||
) -> Dict[str, Any]:
|
||||
"""从一个源分配到多个目标
|
||||
|
||||
前端效果:
|
||||
- source: 单选下拉框
|
||||
- targets: 多选下拉框(可选择多个容器)
|
||||
- volumes: 数组输入(每个目标对应一个体积)
|
||||
"""
|
||||
results = []
|
||||
for target, vol in zip(targets, volumes):
|
||||
print(f"Dispensing {vol}μL to {target.name}")
|
||||
results.append({
|
||||
"target": target.id,
|
||||
"volume": vol
|
||||
})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"dispense_results": results
|
||||
}
|
||||
|
||||
def test_with_balance(
|
||||
self,
|
||||
target: ResourceSlot, # 容器
|
||||
balance: DeviceSlot # 天平设备
|
||||
) -> Dict[str, Any]:
|
||||
"""使用天平测量容器
|
||||
|
||||
前端效果:
|
||||
- target: 容器选择下拉框
|
||||
- balance: 设备选择下拉框(仅显示天平类型)
|
||||
"""
|
||||
print(f"Weighing {target.name} on {balance.name}")
|
||||
|
||||
# 可以调用balance的方法
|
||||
# weight = balance.get_weight()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"container": target.id,
|
||||
"balance_used": balance.id
|
||||
}
|
||||
```
|
||||
|
||||
### 工作原理
|
||||
|
||||
#### 1. 类型识别
|
||||
|
||||
注册表扫描方法签名时:
|
||||
|
||||
```python
|
||||
def my_method(self, resource: ResourceSlot, device: DeviceSlot):
|
||||
pass
|
||||
```
|
||||
|
||||
系统识别到`ResourceSlot`和`DeviceSlot`类型。
|
||||
|
||||
#### 2. 自动添加 placeholder_keys
|
||||
|
||||
在注册表中自动生成:
|
||||
|
||||
```yaml
|
||||
my_device:
|
||||
class:
|
||||
action_value_mappings:
|
||||
my_method:
|
||||
goal:
|
||||
resource: resource
|
||||
device: device
|
||||
placeholder_keys:
|
||||
resource: unilabos_resources # 自动添加!
|
||||
device: unilabos_devices # 自动添加!
|
||||
```
|
||||
|
||||
#### 3. 前端 UI 生成
|
||||
|
||||
- `unilabos_resources`: 渲染为资源选择下拉框
|
||||
- `unilabos_devices`: 渲染为设备选择下拉框
|
||||
|
||||
#### 4. 运行时解析
|
||||
|
||||
用户选择资源/设备后,实际调用时会传入完整的资源/设备对象:
|
||||
|
||||
```python
|
||||
# 用户在前端选择了 plate_1
|
||||
# 运行时,source参数会收到完整的Resource对象
|
||||
source.id # "plate_1"
|
||||
source.name # "96孔板"
|
||||
source.type # "resource"
|
||||
source.class_ # "corning_96_wellplate_360ul_flat"
|
||||
```
|
||||
|
||||
## 支持的通信方式
|
||||
|
||||
### 1. 串口(Serial)
|
||||
|
||||
```python
|
||||
import serial
|
||||
|
||||
class SerialDevice:
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
self.port = config['port']
|
||||
self.baudrate = config.get('baudrate', 9600)
|
||||
self.ser = serial.Serial(
|
||||
port=self.port,
|
||||
baudrate=self.baudrate,
|
||||
timeout=1
|
||||
)
|
||||
|
||||
def send_command(self, cmd: str) -> str:
|
||||
"""发送命令并读取响应"""
|
||||
self.ser.write(f"{cmd}\r\n".encode())
|
||||
response = self.ser.readline().decode().strip()
|
||||
return response
|
||||
|
||||
def __del__(self):
|
||||
if hasattr(self, 'ser') and self.ser.is_open:
|
||||
self.ser.close()
|
||||
```
|
||||
|
||||
### 2. TCP/IP Socket
|
||||
|
||||
```python
|
||||
import socket
|
||||
|
||||
class TCPDevice:
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
self.host = config['host']
|
||||
self.port = config['port']
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.sock.connect((self.host, self.port))
|
||||
|
||||
def send_command(self, cmd: str) -> str:
|
||||
self.sock.sendall(cmd.encode())
|
||||
response = self.sock.recv(1024).decode()
|
||||
return response
|
||||
```
|
||||
|
||||
### 3. Modbus
|
||||
|
||||
```python
|
||||
from pymodbus.client import ModbusTcpClient
|
||||
|
||||
class ModbusDevice:
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
self.host = config['host']
|
||||
self.port = config.get('port', 502)
|
||||
self.client = ModbusTcpClient(self.host, port=self.port)
|
||||
self.client.connect()
|
||||
|
||||
def read_register(self, address: int) -> int:
|
||||
result = self.client.read_holding_registers(address, 1)
|
||||
return result.registers[0]
|
||||
|
||||
def write_register(self, address: int, value: int):
|
||||
self.client.write_register(address, value)
|
||||
```
|
||||
|
||||
### 4. OPC UA
|
||||
|
||||
```python
|
||||
from opcua import Client
|
||||
|
||||
class OPCUADevice:
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
self.url = config['url']
|
||||
self.client = Client(self.url)
|
||||
self.client.connect()
|
||||
|
||||
def read_node(self, node_id: str):
|
||||
node = self.client.get_node(node_id)
|
||||
return node.get_value()
|
||||
|
||||
def write_node(self, node_id: str, value):
|
||||
node = self.client.get_node(node_id)
|
||||
node.set_value(value)
|
||||
```
|
||||
|
||||
### 5. HTTP/RPC
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
class HTTPDevice:
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
self.base_url = config['url']
|
||||
self.auth_token = config.get('token')
|
||||
|
||||
def send_command(self, endpoint: str, data: Dict) -> Dict:
|
||||
url = f"{self.base_url}/{endpoint}"
|
||||
headers = {'Authorization': f'Bearer {self.auth_token}'}
|
||||
response = requests.post(url, json=data, headers=headers)
|
||||
return response.json()
|
||||
```
|
||||
|
||||
## 异步 vs 同步方法
|
||||
|
||||
### 同步方法(适合快速操作)
|
||||
|
||||
```python
|
||||
def quick_operation(self, param: float) -> Dict[str, Any]:
|
||||
"""快速操作,立即返回"""
|
||||
result = self._do_something(param)
|
||||
return {"success": True, "result": result}
|
||||
```
|
||||
|
||||
### 异步方法(适合耗时操作)
|
||||
|
||||
```python
|
||||
async def long_operation(self, duration: float) -> Dict[str, Any]:
|
||||
"""长时间运行的操作"""
|
||||
self._status = "running"
|
||||
|
||||
# 使用 ROS2 提供的 sleep 方法(而不是 asyncio.sleep)
|
||||
await self.sleep(duration)
|
||||
|
||||
# 可以在过程中发送feedback
|
||||
# 需要配合ROS2 Action的feedback机制
|
||||
|
||||
self._status = "idle"
|
||||
return {"success": True, "duration": duration}
|
||||
```
|
||||
|
||||
> **⚠️ 重要提示:ROS2 异步机制 vs Python asyncio**
|
||||
>
|
||||
> Uni-Lab 的设备驱动虽然使用 `async def` 语法,但**底层是 ROS2 的异步机制,而不是 Python 的 asyncio**。
|
||||
>
|
||||
> **不能使用的 asyncio 功能:**
|
||||
>
|
||||
> - ❌ `asyncio.sleep()` - 会导致 ROS2 事件循环阻塞
|
||||
> - ❌ `asyncio.create_task()` - 任务不会被 ROS2 正确调度
|
||||
> - ❌ `asyncio.gather()` - 无法与 ROS2 集成
|
||||
> - ❌ 其他 asyncio 标准库函数
|
||||
>
|
||||
> **应该使用的方法(继承自 BaseROS2DeviceNode):**
|
||||
>
|
||||
> - ✅ `await self.sleep(seconds)` - ROS2 兼容的睡眠
|
||||
> - ✅ `await self.create_task(func, **kwargs)` - ROS2 兼容的任务创建
|
||||
> - ✅ ROS2 的 Action/Service 回调机制
|
||||
>
|
||||
> **示例:**
|
||||
>
|
||||
> ```python
|
||||
> async def complex_operation(self, duration: float) -> Dict[str, Any]:
|
||||
> """正确使用 ROS2 异步方法"""
|
||||
> self._status = "processing"
|
||||
>
|
||||
> # ✅ 正确:使用 self.sleep
|
||||
> await self.sleep(duration)
|
||||
>
|
||||
> # ✅ 正确:创建并发任务
|
||||
> task = await self.create_task(self._background_work)
|
||||
>
|
||||
> # ❌ 错误:不要使用 asyncio
|
||||
> # await asyncio.sleep(duration) # 这会导致问题!
|
||||
> # task = asyncio.create_task(...) # 这也不行!
|
||||
>
|
||||
> self._status = "idle"
|
||||
> return {"success": True}
|
||||
>
|
||||
> async def _background_work(self):
|
||||
> """后台任务"""
|
||||
> await self.sleep(1.0)
|
||||
> self.lab_logger().info("Background work completed")
|
||||
> ```
|
||||
>
|
||||
> **为什么不能混用?**
|
||||
>
|
||||
> ROS2 使用 `rclpy` 的事件循环来管理所有异步操作。如果使用 `asyncio` 的函数,这些操作会在不同的事件循环中运行,导致:
|
||||
>
|
||||
> - ROS2 回调无法正确执行
|
||||
> - 任务可能永远不会完成
|
||||
> - 程序可能死锁或崩溃
|
||||
>
|
||||
> **参考实现:**
|
||||
>
|
||||
> `BaseROS2DeviceNode` 提供的方法定义(`base_device_node.py:563-572`):
|
||||
>
|
||||
> ```python
|
||||
> async def sleep(self, rel_time: float, callback_group=None):
|
||||
> """ROS2 兼容的异步睡眠"""
|
||||
> if callback_group is None:
|
||||
> callback_group = self.callback_group
|
||||
> await ROS2DeviceNode.async_wait_for(self, rel_time, callback_group)
|
||||
>
|
||||
> @classmethod
|
||||
> async def create_task(cls, func, trace_error=True, **kwargs) -> Task:
|
||||
> """ROS2 兼容的任务创建"""
|
||||
> return ROS2DeviceNode.run_async_func(func, trace_error, **kwargs)
|
||||
> ```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 基本错误处理
|
||||
|
||||
```python
|
||||
def operation_with_error_handling(self, param: float) -> Dict[str, Any]:
|
||||
"""带错误处理的操作"""
|
||||
try:
|
||||
result = self._risky_operation(param)
|
||||
return {
|
||||
"success": True,
|
||||
"result": result
|
||||
}
|
||||
except ValueError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Invalid parameter",
|
||||
"message": str(e)
|
||||
}
|
||||
except IOError as e:
|
||||
self._status = "error"
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Communication error",
|
||||
"message": str(e)
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义异常
|
||||
|
||||
```python
|
||||
class DeviceError(Exception):
|
||||
"""设备错误基类"""
|
||||
pass
|
||||
|
||||
class DeviceNotReadyError(DeviceError):
|
||||
"""设备未就绪"""
|
||||
pass
|
||||
|
||||
class DeviceTimeoutError(DeviceError):
|
||||
"""设备超时"""
|
||||
pass
|
||||
|
||||
class MyDevice:
|
||||
def operation(self) -> Dict[str, Any]:
|
||||
if self._status != "idle":
|
||||
raise DeviceNotReadyError(f"Device is {self._status}")
|
||||
|
||||
# 执行操作
|
||||
return {"success": True}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 类型注解
|
||||
|
||||
```python
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
def method(
|
||||
self,
|
||||
param1: float,
|
||||
param2: str,
|
||||
optional_param: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""完整的类型注解有助于自动生成注册表"""
|
||||
pass
|
||||
```
|
||||
|
||||
### 2. 文档字符串
|
||||
|
||||
```python
|
||||
def method(self, param: float) -> Dict[str, Any]:
|
||||
"""方法简短描述
|
||||
|
||||
更详细的说明...
|
||||
|
||||
Args:
|
||||
param: 参数说明,包括单位和范围
|
||||
|
||||
Returns:
|
||||
Dict包含:
|
||||
- success (bool): 是否成功
|
||||
- result (Any): 结果数据
|
||||
|
||||
Raises:
|
||||
DeviceError: 错误情况说明
|
||||
"""
|
||||
pass
|
||||
```
|
||||
|
||||
### 3. 配置验证
|
||||
|
||||
```python
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
# 验证必需参数
|
||||
required = ['port', 'baudrate']
|
||||
for key in required:
|
||||
if key not in config:
|
||||
raise ValueError(f"Missing required config: {key}")
|
||||
|
||||
self.port = config['port']
|
||||
self.baudrate = config['baudrate']
|
||||
```
|
||||
|
||||
### 4. 资源清理
|
||||
|
||||
```python
|
||||
def __del__(self):
|
||||
"""析构函数,清理资源"""
|
||||
if hasattr(self, 'connection') and self.connection:
|
||||
self.connection.close()
|
||||
```
|
||||
|
||||
### 5. 设计前端友好的返回值
|
||||
|
||||
**记住:返回值会直接显示在 Web 界面**
|
||||
|
||||
```python
|
||||
import time
|
||||
|
||||
def measure_temperature(self) -> Dict[str, Any]:
|
||||
"""测量温度
|
||||
|
||||
✅ 好的返回值设计:
|
||||
- 包含 success 状态
|
||||
- 使用描述性键名
|
||||
- 在键名中包含单位
|
||||
- 记录测量时间
|
||||
"""
|
||||
temp = self._read_temperature()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"temperature_celsius": temp, # 键名包含单位
|
||||
"timestamp": time.time(), # 记录时间
|
||||
"sensor_status": "normal", # 额外状态信息
|
||||
"message": f"温度测量完成: {temp}°C" # 用户友好的消息
|
||||
}
|
||||
|
||||
def bad_example(self) -> Dict[str, Any]:
|
||||
"""❌ 不好的返回值设计"""
|
||||
return {
|
||||
"s": True, # ❌ 键名不明确
|
||||
"v": 25.5, # ❌ 没有说明单位
|
||||
"t": 1234567890, # ❌ 不清楚是什么时间戳
|
||||
}
|
||||
```
|
||||
|
||||
**参考 `host_node.test_latency` 方法**(第 1216-1340 行),它返回详细的测试结果,在前端清晰显示:
|
||||
|
||||
```python
|
||||
return {
|
||||
"status": "success",
|
||||
"avg_rtt_ms": 25.5, # 有意义的键名 + 单位
|
||||
"avg_time_diff_ms": 10.2,
|
||||
"max_time_error_ms": 5.3,
|
||||
"task_delay_ms": 15.7,
|
||||
"test_count": 5, # 记录重要信息
|
||||
}
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
看完本文档后,建议继续阅读:
|
||||
|
||||
- {doc}`add_action` - 了解如何添加新的动作指令
|
||||
- {doc}`add_yaml` - 学习如何编写和完善 YAML 注册表
|
||||
|
||||
进阶主题:
|
||||
|
||||
- {doc}`03_add_device_registry` - 了解如何配置注册表
|
||||
- {doc}`04_add_device_testing` - 学习如何测试设备
|
||||
- {doc}`add_old_device` - 没有 SDK 时如何开发设备驱动
|
||||
|
||||
## 参考
|
||||
|
||||
- [Python 类型注解](https://docs.python.org/3/library/typing.html)
|
||||
- [ROS2 rclpy 异步编程](https://docs.ros.org/en/humble/Tutorials/Intermediate/Writing-an-Action-Server-Client/Py.html) - Uni-Lab 使用 ROS2 的异步机制
|
||||
- [串口通信](https://pyserial.readthedocs.io/)
|
||||
|
||||
> **注意:** 虽然设备驱动使用 `async def` 语法,但请**不要参考** Python 标准的 [asyncio 文档](https://docs.python.org/3/library/asyncio.html)。Uni-Lab 使用的是 ROS2 的异步机制,两者不兼容。请使用 `self.sleep()` 和 `self.create_task()` 等 BaseROS2DeviceNode 提供的方法。
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# 设备 Driver 开发
|
||||
# 设备 Driver 开发(无 SDK 设备)
|
||||
|
||||
我们对设备 Driver 的定义,是一个 Python/C++/C# 类,类的方法可以用于获取传感器数据、执行设备动作、更新物料信息。它们经过 Uni-Lab 的通信中间件包装,就能成为高效分布式通信的设备节点。
|
||||
|
||||
因此,若已有设备的 SDK (Driver),可以直接 [添加进 Uni-Lab](add_device.md)。仅当没有 SDK (Driver) 时,请参考本章作开发。
|
||||
因此,若已有设备的 SDK (Driver),可以直接 [添加进 Uni-Lab](add_device.md)。**仅当没有 SDK (Driver) 时,请参考本章进行驱动开发。**
|
||||
|
||||
> **💡 提示:** 本文档介绍如何为没有现成驱动的老设备开发驱动程序。如果您的设备已经有 SDK 或驱动,请直接参考 {doc}`add_device`。
|
||||
|
||||
## 有串口字符串指令集文档的设备:Python 串口通信(常见 RS485, RS232, USB)
|
||||
|
||||
@@ -12,13 +14,13 @@
|
||||
|
||||
Modbus 与 RS485、RS232 不一样的地方在于,会有更多直接寄存器的读写,以及涉及字节序转换(Big Endian, Little Endian)。
|
||||
|
||||
Uni-Lab 开发团队在仓库中提供了3个样例:
|
||||
Uni-Lab 开发团队在仓库中提供了 3 个样例:
|
||||
|
||||
* 单一机械设备**电夹爪**,通讯协议可见 [增广夹爪通讯协议](https://doc.rmaxis.com/docs/communication/fieldbus/),驱动代码位于 `unilabos/devices/gripper/rmaxis_v4.py`
|
||||
* 单一通信设备**IO板卡**,驱动代码位于 `unilabos/device_comms/gripper/SRND_16_IO.py`
|
||||
* 执行多设备复杂任务逻辑的**PLC**,Uni-Lab 提供了基于地址表的接入方式和点动工作流编写,测试代码位于 `unilabos/device_comms/modbus_plc/test/test_workflow.py`
|
||||
- 单一机械设备**电夹爪**,通讯协议可见 [增广夹爪通讯协议](https://doc.rmaxis.com/docs/communication/fieldbus/),驱动代码位于 `unilabos/devices/gripper/rmaxis_v4.py`
|
||||
- 单一通信设备**IO 板卡**,驱动代码位于 `unilabos/device_comms/gripper/SRND_16_IO.py`
|
||||
- 执行多设备复杂任务逻辑的**PLC**,Uni-Lab 提供了基于地址表的接入方式和点动工作流编写,测试代码位于 `unilabos/device_comms/modbus_plc/test/test_workflow.py`
|
||||
|
||||
****
|
||||
---
|
||||
|
||||
## 其他工业通信协议:CANopen, Ethernet, OPCUA...
|
||||
|
||||
@@ -26,32 +28,32 @@ Uni-Lab 开发团队在仓库中提供了3个样例:
|
||||
|
||||
## 没有接口的老设备老软件:使用 PyWinAuto
|
||||
|
||||
**pywinauto**是一个 Python 库,用于自动化Windows GUI操作。它可以模拟用户的鼠标点击、键盘输入、窗口操作等,广泛应用于自动化测试、GUI自动化等场景。它支持通过两个后端进行操作:
|
||||
**pywinauto**是一个 Python 库,用于自动化 Windows GUI 操作。它可以模拟用户的鼠标点击、键盘输入、窗口操作等,广泛应用于自动化测试、GUI 自动化等场景。它支持通过两个后端进行操作:
|
||||
|
||||
* **win32**后端:适用于大多数Windows应用程序,使用native Win32 API。(pywinauto_recorder默认使用win32后端)
|
||||
* **uia**后端:基于Microsoft UI Automation,适用于较新的应用程序,特别是基于WPF或UWP的应用程序。(在win10上,会有更全的目录,有的窗口win32会识别不到)
|
||||
- **win32**后端:适用于大多数 Windows 应用程序,使用 native Win32 API。(pywinauto_recorder 默认使用 win32 后端)
|
||||
- **uia**后端:基于 Microsoft UI Automation,适用于较新的应用程序,特别是基于 WPF 或 UWP 的应用程序。(在 win10 上,会有更全的目录,有的窗口 win32 会识别不到)
|
||||
|
||||
### windows平台安装pywinauto和pywinauto_recorder
|
||||
### windows 平台安装 pywinauto 和 pywinauto_recorder
|
||||
|
||||
直接安装会造成环境崩溃,需要下载并解压已经修改好的文件。
|
||||
|
||||
cd到对应目录,执行安装
|
||||
cd 到对应目录,执行安装
|
||||
|
||||
`pip install . -i ``https://pypi.tuna.tsinghua.edu.cn/simple`
|
||||
` pip install . -i ``https://pypi.tuna.tsinghua.edu.cn/simple `
|
||||
|
||||

|
||||
|
||||
windows平台测试 python pywinauto_recorder.py,退出使用两次ctrl+alt+r取消选中,关闭命令提示符。
|
||||
windows 平台测试 python pywinauto_recorder.py,退出使用两次 ctrl+alt+r 取消选中,关闭命令提示符。
|
||||
|
||||
### 计算器例子
|
||||
|
||||
你可以先打开windows的计算器,然后在ilab的环境中运行下面的代码片段,可观察到得到结果,通过这一案例,你需要掌握的pywinauto用法:
|
||||
你可以先打开 windows 的计算器,然后在 ilab 的环境中运行下面的代码片段,可观察到得到结果,通过这一案例,你需要掌握的 pywinauto 用法:
|
||||
|
||||
* 连接到指定进程
|
||||
* 利用dump_tree查找需要的窗口
|
||||
* 获取某个位置的信息
|
||||
* 模拟点击
|
||||
* 模拟输入
|
||||
- 连接到指定进程
|
||||
- 利用 dump_tree 查找需要的窗口
|
||||
- 获取某个位置的信息
|
||||
- 模拟点击
|
||||
- 模拟输入
|
||||
|
||||
#### 代码学习
|
||||
|
||||
@@ -74,39 +76,39 @@ window.dump_tree(depth=3)
|
||||
Dialog - '计算器' (L-419, T773, R-73, B1287)
|
||||
['计算器Dialog', 'Dialog', '计算器', '计算器Dialog0', '计算器Dialog1', 'Dialog0', 'Dialog1', '计算器0', '计算器1']
|
||||
child_window(title="计算器", control_type="Window")
|
||||
|
|
||||
|
|
||||
| Dialog - '计算器' (L-269, T774, R-81, B806)
|
||||
| ['计算器Dialog2', 'Dialog2', '计算器2']
|
||||
| child_window(title="计算器", auto_id="TitleBar", control_type="Window")
|
||||
| |
|
||||
| |
|
||||
| | Menu - '系统' (L0, T0, R0, B0)
|
||||
| | ['Menu', '系统', '系统Menu', '系统0', '系统1']
|
||||
| | child_window(title="系统", auto_id="SystemMenuBar", control_type="MenuBar")
|
||||
| |
|
||||
| |
|
||||
| | Button - '最小化 计算器' (L-219, T774, R-173, B806)
|
||||
| | ['Button', '最小化 计算器Button', '最小化 计算器', 'Button0', 'Button1']
|
||||
| | child_window(title="最小化 计算器", auto_id="Minimize", control_type="Button")
|
||||
| |
|
||||
| |
|
||||
| | Button - '使 计算器 最大化' (L-173, T774, R-127, B806)
|
||||
| | ['Button2', '使 计算器 最大化', '使 计算器 最大化Button']
|
||||
| | child_window(title="使 计算器 最大化", auto_id="Maximize", control_type="Button")
|
||||
| |
|
||||
| |
|
||||
| | Button - '关闭 计算器' (L-127, T774, R-81, B806)
|
||||
| | ['Button3', '关闭 计算器Button', '关闭 计算器']
|
||||
| | child_window(title="关闭 计算器", auto_id="Close", control_type="Button")
|
||||
|
|
||||
|
|
||||
| Dialog - '计算器' (L-411, T774, R-81, B1279)
|
||||
| ['计算器Dialog3', 'Dialog3', '计算器3']
|
||||
| child_window(title="计算器", control_type="Window")
|
||||
| |
|
||||
| |
|
||||
| | Static - '计算器' (L-363, T782, R-327, B798)
|
||||
| | ['计算器Static', 'Static', '计算器4', 'Static0', 'Static1']
|
||||
| | child_window(title="计算器", auto_id="AppName", control_type="Text")
|
||||
| |
|
||||
| |
|
||||
| | Custom - '' (L-411, T806, R-81, B1279)
|
||||
| | ['Custom', '计算器Custom']
|
||||
| | child_window(auto_id="NavView", control_type="Custom")
|
||||
|
|
||||
|
|
||||
| Pane - '' (L-411, T806, R-81, B1279)
|
||||
| ['Pane', '计算器Pane']
|
||||
"""
|
||||
@@ -122,58 +124,58 @@ target_window.dump_tree(depth=3)
|
||||
Custom - '' (L-411, T806, R-81, B1279)
|
||||
['标准Custom', 'Custom']
|
||||
child_window(auto_id="NavView", control_type="Custom")
|
||||
|
|
||||
|
|
||||
| Button - '打开导航' (L-407, T812, R-367, B848)
|
||||
| ['打开导航Button', '打开导航', 'Button', 'Button0', 'Button1']
|
||||
| child_window(title="打开导航", auto_id="TogglePaneButton", control_type="Button")
|
||||
| |
|
||||
| |
|
||||
| | Static - '' (L0, T0, R0, B0)
|
||||
| | ['Static', 'Static0', 'Static1']
|
||||
| | child_window(auto_id="PaneTitleTextBlock", control_type="Text")
|
||||
|
|
||||
|
|
||||
| GroupBox - '' (L-411, T814, R-81, B1275)
|
||||
| ['标准GroupBox', 'GroupBox', 'GroupBox0', 'GroupBox1']
|
||||
| |
|
||||
| |
|
||||
| | Static - '表达式为 ' (L0, T0, R0, B0)
|
||||
| | ['表达式为 ', 'Static2', '表达式为 Static']
|
||||
| | child_window(title="表达式为 ", auto_id="CalculatorExpression", control_type="Text")
|
||||
| |
|
||||
| |
|
||||
| | Static - '显示为 0' (L-411, T875, R-81, B947)
|
||||
| | ['显示为 0Static', '显示为 0', 'Static3']
|
||||
| | child_window(title="显示为 0", auto_id="CalculatorResults", control_type="Text")
|
||||
| |
|
||||
| |
|
||||
| | Button - '打开历史记录浮出控件' (L-121, T814, R-89, B846)
|
||||
| | ['打开历史记录浮出控件', '打开历史记录浮出控件Button', 'Button2']
|
||||
| | child_window(title="打开历史记录浮出控件", auto_id="HistoryButton", control_type="Button")
|
||||
| |
|
||||
| |
|
||||
| | GroupBox - '记忆控件' (L-407, T948, R-85, B976)
|
||||
| | ['记忆控件', '记忆控件GroupBox', 'GroupBox2']
|
||||
| | child_window(title="记忆控件", auto_id="MemoryPanel", control_type="Group")
|
||||
| |
|
||||
| |
|
||||
| | GroupBox - '显示控件' (L-407, T978, R-85, B1026)
|
||||
| | ['显示控件', 'GroupBox3', '显示控件GroupBox']
|
||||
| | child_window(title="显示控件", auto_id="DisplayControls", control_type="Group")
|
||||
| |
|
||||
| |
|
||||
| | GroupBox - '标准函数' (L-407, T1028, R-166, B1076)
|
||||
| | ['标准函数', '标准函数GroupBox', 'GroupBox4']
|
||||
| | child_window(title="标准函数", auto_id="StandardFunctions", control_type="Group")
|
||||
| |
|
||||
| |
|
||||
| | GroupBox - '标准运算符' (L-164, T1028, R-85, B1275)
|
||||
| | ['标准运算符', '标准运算符GroupBox', 'GroupBox5']
|
||||
| | child_window(title="标准运算符", auto_id="StandardOperators", control_type="Group")
|
||||
| |
|
||||
| |
|
||||
| | GroupBox - '数字键盘' (L-407, T1078, R-166, B1275)
|
||||
| | ['GroupBox6', '数字键盘', '数字键盘GroupBox']
|
||||
| | child_window(title="数字键盘", auto_id="NumberPad", control_type="Group")
|
||||
| |
|
||||
| |
|
||||
| | Button - '正负' (L-407, T1228, R-328, B1275)
|
||||
| | ['Button32', '正负Button', '正负']
|
||||
| | child_window(title="正负", auto_id="negateButton", control_type="Button")
|
||||
|
|
||||
|
|
||||
| Static - '标准' (L-363, T815, R-322, B842)
|
||||
| ['标准', '标准Static', 'Static4']
|
||||
| child_window(title="标准", auto_id="Header", control_type="Text")
|
||||
|
|
||||
|
|
||||
| Button - '始终置顶' (L-312, T814, R-280, B846)
|
||||
| ['始终置顶Button', '始终置顶', 'Button33']
|
||||
| child_window(title="始终置顶", auto_id="NormalAlwaysOnTopButton", control_type="Button")
|
||||
@@ -187,47 +189,47 @@ numpad.dump_tree(depth=2)
|
||||
GroupBox - '数字键盘' (L-334, T1350, R-93, B1547)
|
||||
['GroupBox', '数字键盘', '数字键盘GroupBox']
|
||||
child_window(title="数字键盘", auto_id="NumberPad", control_type="Group")
|
||||
|
|
||||
|
|
||||
| Button - '零' (L-253, T1500, R-174, B1547)
|
||||
| ['零Button', 'Button', '零', 'Button0', 'Button1']
|
||||
| child_window(title="零", auto_id="num0Button", control_type="Button")
|
||||
|
|
||||
|
|
||||
| Button - '一' (L-334, T1450, R-255, B1498)
|
||||
| ['一Button', 'Button2', '一']
|
||||
| child_window(title="一", auto_id="num1Button", control_type="Button")
|
||||
|
|
||||
|
|
||||
| Button - '二' (L-253, T1450, R-174, B1498)
|
||||
| ['Button3', '二', '二Button']
|
||||
| child_window(title="二", auto_id="num2Button", control_type="Button")
|
||||
|
|
||||
|
|
||||
| Button - '三' (L-172, T1450, R-93, B1498)
|
||||
| ['Button4', '三', '三Button']
|
||||
| child_window(title="三", auto_id="num3Button", control_type="Button")
|
||||
|
|
||||
|
|
||||
| Button - '四' (L-334, T1400, R-255, B1448)
|
||||
| ['四', 'Button5', '四Button']
|
||||
| child_window(title="四", auto_id="num4Button", control_type="Button")
|
||||
|
|
||||
|
|
||||
| Button - '五' (L-253, T1400, R-174, B1448)
|
||||
| ['Button6', '五Button', '五']
|
||||
| child_window(title="五", auto_id="num5Button", control_type="Button")
|
||||
|
|
||||
|
|
||||
| Button - '六' (L-172, T1400, R-93, B1448)
|
||||
| ['六Button', 'Button7', '六']
|
||||
| child_window(title="六", auto_id="num6Button", control_type="Button")
|
||||
|
|
||||
|
|
||||
| Button - '七' (L-334, T1350, R-255, B1398)
|
||||
| ['Button8', '七Button', '七']
|
||||
| child_window(title="七", auto_id="num7Button", control_type="Button")
|
||||
|
|
||||
|
|
||||
| Button - '八' (L-253, T1350, R-174, B1398)
|
||||
| ['八', 'Button9', '八Button']
|
||||
| child_window(title="八", auto_id="num8Button", control_type="Button")
|
||||
|
|
||||
|
|
||||
| Button - '九' (L-172, T1350, R-93, B1398)
|
||||
| ['Button10', '九', '九Button']
|
||||
| child_window(title="九", auto_id="num9Button", control_type="Button")
|
||||
|
|
||||
|
|
||||
| Button - '十进制分隔符' (L-172, T1500, R-93, B1547)
|
||||
| ['十进制分隔符Button', 'Button11', '十进制分隔符']
|
||||
| child_window(title="十进制分隔符", auto_id="decimalSeparatorButton", control_type="Button")
|
||||
@@ -262,13 +264,13 @@ r, g, b = pyautogui.pixel(point_x, point_y)
|
||||
|
||||
### pywinauto_recorder
|
||||
|
||||
pywinauto_recorder是一个配合 pywinauto 使用的工具,用于录制用户的操作,并生成相应的 pywinauto 脚本。这对于一些暂时无法直接调用DLL的函数并且需要模拟用户操作的场景非常有用。同时,可以省去仅用pywinauto的一些查找UI步骤。
|
||||
pywinauto_recorder 是一个配合 pywinauto 使用的工具,用于录制用户的操作,并生成相应的 pywinauto 脚本。这对于一些暂时无法直接调用 DLL 的函数并且需要模拟用户操作的场景非常有用。同时,可以省去仅用 pywinauto 的一些查找 UI 步骤。
|
||||
|
||||
#### 运行尝试
|
||||
|
||||
请参照 上手尝试-环境创建-3 开启pywinauto_recorder
|
||||
请参照 上手尝试-环境创建-3 开启 pywinauto_recorder
|
||||
|
||||
例如我们这里先启动一个windows自带的计算器软件
|
||||
例如我们这里先启动一个 windows 自带的计算器软件
|
||||
|
||||

|
||||
|
||||
@@ -286,7 +288,7 @@ with UIPath(u"计算器||Window"):
|
||||
click(u"九||Button")
|
||||
```
|
||||
|
||||
执行该python脚本,可以观察到新开启的计算器被点击了数字9
|
||||
执行该 python 脚本,可以观察到新开启的计算器被点击了数字 9
|
||||
|
||||

|
||||
|
||||
@@ -308,23 +310,38 @@ window.dump_tree(depth=[int类型数字], filename=None)
|
||||
GroupBox - '数字键盘' (L-334, T1350, R-93, B1547)
|
||||
['GroupBox', '数字键盘', '数字键盘GroupBox']
|
||||
child_window(title="数字键盘", auto_id="NumberPad", control_type="Group")
|
||||
|
|
||||
|
|
||||
| Button - '零' (L-253, T1500, R-174, B1547)
|
||||
| ['零Button', 'Button', '零', 'Button0', 'Button1']
|
||||
| child_window(title="零", auto_id="num0Button", control_type="Button")
|
||||
"""
|
||||
```
|
||||
|
||||
这里以上面计算器的例子对dump_tree进行解读
|
||||
这里以上面计算器的例子对 dump_tree 进行解读
|
||||
|
||||
2~4行为当前对象的窗口
|
||||
2~4 行为当前对象的窗口
|
||||
|
||||
* 第2行分别是窗体的类型 `GroupBox`,窗体的题目 `数字键盘`,窗体的矩形区域坐标,对应的是屏幕上的位置(左、上、右、下)
|
||||
* 第3行是 `['GroupBox', '数字键盘', '数字键盘GroupBox']`,为控件的标识符列表,可以选择任意一个,使用 `child_window(best_match="标识符")`来获取该窗口
|
||||
* 第4行是获取该控件的方法,请注意该方法不能保证获取唯一,`title`如果是变化的,也需要删除 `title`参数
|
||||
- 第 2 行分别是窗体的类型 `GroupBox`,窗体的题目 `数字键盘`,窗体的矩形区域坐标,对应的是屏幕上的位置(左、上、右、下)
|
||||
- 第 3 行是 `['GroupBox', '数字键盘', '数字键盘GroupBox']`,为控件的标识符列表,可以选择任意一个,使用 `child_window(best_match="标识符")`来获取该窗口
|
||||
- 第 4 行是获取该控件的方法,请注意该方法不能保证获取唯一,`title`如果是变化的,也需要删除 `title`参数
|
||||
|
||||
6~8行为当前对象窗口所包含的子窗口信息,信息类型对应2~4行
|
||||
6~8 行为当前对象窗口所包含的子窗口信息,信息类型对应 2~4 行
|
||||
|
||||
### 窗口获取注意事项
|
||||
|
||||
1. 在 `child_window`的时候,并不会立刻报错,只有在执行窗口的信息获取时才会调用,查询窗口是否存在,因此要想确定 `child_window`是否正确,可以调用子窗口对象的属性 `element_info`,来保证窗口存在
|
||||
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` - 设备测试指南
|
||||
1118
docs/developer_guide/add_registry.md
Normal file
1118
docs/developer_guide/add_registry.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,17 @@
|
||||
# 电池装配工站接入(PLC)
|
||||
# 实例:电池装配工站接入(PLC控制)
|
||||
|
||||
本指南将引导你完成电池装配工站(以 PLC 控制为例)的接入流程,包括新建工站文件、编写驱动与寄存器读写、生成注册表、上传及注意事项。
|
||||
> **文档类型**:实际应用案例
|
||||
> **适用场景**:使用 PLC 控制的电池装配工站接入
|
||||
> **前置知识**:{doc}`../add_device` | {doc}`../add_registry`
|
||||
|
||||
本指南以电池装配工站为实际案例,引导你完成 PLC 控制设备的完整接入流程,包括新建工站文件、编写驱动与寄存器读写、生成注册表、上传及注意事项。
|
||||
|
||||
## 案例概述
|
||||
|
||||
**设备类型**:电池装配工站
|
||||
**通信方式**:Modbus TCP (PLC)
|
||||
**工站基类**:`WorkstationBase`
|
||||
**主要功能**:电池组装、寄存器读写、数据采集
|
||||
|
||||
## 1. 新建工站文件
|
||||
|
||||
@@ -93,10 +104,12 @@ python unilabos\app\main.py -g celljson.json --ak <user的AK> --sk <user的SK>
|
||||
```
|
||||
|
||||
点击注册表编辑,进入注册表编辑页面
|
||||

|
||||
|
||||

|
||||
|
||||
按照图示步骤填写自动生成注册表信息:
|
||||

|
||||
|
||||

|
||||
|
||||
步骤说明:
|
||||
1. 选择新增的工站`coin_cell_assembly.py`文件
|
||||
@@ -107,8 +120,9 @@ python unilabos\app\main.py -g celljson.json --ak <user的AK> --sk <user的SK>
|
||||
6. 填写新的工站注册表备注信息
|
||||
7. 生成注册表
|
||||
|
||||
以上操作步骤完成,则会生成的新的注册表ymal文件,如下图:
|
||||

|
||||
以上操作步骤完成,则会生成的新的注册表YAML文件,如下图:
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
@@ -134,14 +148,60 @@ python unilabos\app\main.py -g celljson.json --ak <user的AK> --sk <user的SK> -
|
||||
|
||||
## 4. 注意事项
|
||||
|
||||
- 在新生成的 YAML 中,确认 `module` 指向新工站类,本例中需检查`coincellassemblyworkstation_device.yaml`文件中是否指向了`coin_cell_assembly.py`文件中定义的`CoinCellAssemblyWorkstation`类文件:
|
||||
### 4.1 验证模块路径
|
||||
|
||||
```
|
||||
在新生成的 YAML 中,确认 `module` 指向新工站类。本例中需检查 `coincellassemblyworkstation_device.yaml` 文件中是否正确指向了 `CoinCellAssemblyWorkstation` 类:
|
||||
|
||||
```yaml
|
||||
module: unilabos.devices.workstation.coin_cell_assembly.coin_cell_assembly:CoinCellAssemblyWorkstation
|
||||
```
|
||||
|
||||
- 首次新增设备(或资源)需要在网页端新增注册表信息,`--complete_registry`补全注册表,`--upload_registry`上传注册表信息。
|
||||
### 4.2 首次接入流程
|
||||
|
||||
- 如果不是新增设备(或资源),仅对工站驱动的.py文件进行了修改,则不需要在网页端新增注册表信息。只需要运行补全注册表信息之后,上传注册表即可。
|
||||
首次新增设备(或资源)需要完整流程:
|
||||
1. ✅ 在网页端生成注册表信息
|
||||
2. ✅ 使用 `--complete_registry` 补全注册表
|
||||
3. ✅ 使用 `--upload_registry` 上传注册表信息
|
||||
|
||||
### 4.3 驱动更新流程
|
||||
|
||||
如果不是新增设备,仅修改了工站驱动的 `.py` 文件:
|
||||
1. ✅ 运行 `--complete_registry` 补全注册表
|
||||
2. ✅ 运行 `--upload_registry` 上传注册表
|
||||
3. ❌ 不需要在网页端重新生成注册表
|
||||
|
||||
### 4.4 PLC通信注意事项
|
||||
|
||||
- **握手机制**:若需参数下发,建议在 PLC 端设置标志寄存器并完成握手复位,避免粘连与竞争
|
||||
- **字节序**:FLOAT32 等多字节数据类型需要正确指定字节序(如 `WorderOrder.LITTLE`)
|
||||
- **寄存器映射**:确保 CSV 文件中的寄存器地址与 PLC 实际配置一致
|
||||
- **连接稳定性**:在初始化时检查 PLC 连接状态,建议添加重连机制
|
||||
|
||||
## 5. 扩展阅读
|
||||
|
||||
### 相关文档
|
||||
|
||||
- {doc}`../add_device` - 设备驱动编写通用指南
|
||||
- {doc}`../add_registry` - 注册表配置完整指南
|
||||
- {doc}`../workstation_architecture` - 工站架构详解
|
||||
|
||||
### 技术要点
|
||||
|
||||
- **Modbus TCP 通信**:PLC 通信协议和寄存器读写
|
||||
- **WorkstationBase**:工站基类的继承和使用
|
||||
- **寄存器映射**:CSV 格式的寄存器配置
|
||||
- **注册表生成**:自动化工具使用
|
||||
|
||||
## 6. 总结
|
||||
|
||||
通过本案例,你应该掌握:
|
||||
|
||||
1. ✅ 如何创建 PLC 控制的工站驱动
|
||||
2. ✅ Modbus TCP 通信和寄存器读写
|
||||
3. ✅ 使用可视化编辑器生成注册表
|
||||
4. ✅ 注册表的补全和上传流程
|
||||
5. ✅ 新增设备与更新驱动的区别
|
||||
|
||||
这个案例展示了完整的 PLC 设备接入流程,可以作为其他类似设备接入的参考模板。
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 428 KiB After Width: | Height: | Size: 428 KiB |
|
Before Width: | Height: | Size: 310 KiB After Width: | Height: | Size: 310 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
@@ -1,4 +1,8 @@
|
||||
# 物料构建指南
|
||||
# 实例:物料构建指南
|
||||
|
||||
> **文档类型**:物料系统实战指南
|
||||
> **适用场景**:工作站物料系统构建、Deck/Warehouse/Carrier/Bottle 配置
|
||||
> **前置知识**:PyLabRobot 基础 | 资源管理概念
|
||||
|
||||
## 概述
|
||||
|
||||
@@ -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 结构表现形式及使用场景。
|
||||
|
||||
---
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
# 工作站模板架构设计与对接指南
|
||||
# 实例:工作站模板架构设计与对接指南
|
||||
|
||||
> **文档类型**:架构设计指南与实战案例
|
||||
> **适用场景**:大型工作站接入、子设备管理、物料系统集成
|
||||
> **前置知识**:{doc}`../add_device` | {doc}`../add_registry`
|
||||
|
||||
## 0. 问题简介
|
||||
|
||||
@@ -6,9 +10,9 @@
|
||||
|
||||
### 0.1 自研常量有机工站:最重要的是子设备管理和通信转发
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
这类工站由开发者自研,组合所有子设备和实验耗材、希望让他们在工作站这一级协调配合;
|
||||
|
||||
@@ -18,7 +22,7 @@
|
||||
|
||||
### 0.2 移液工作站:物料系统和工作流模板管理
|
||||
|
||||

|
||||

|
||||
|
||||
1. 绝大多数情况没有子设备,有时候选配恒温震荡等模块时,接口也由工作站提供
|
||||
2. 所有任务系统均由工作站本身实现并下发指令,有统一的抽象函数可实现(pick_up_tips, aspirate, dispense, transfer 等)。有时需要将这些指令组合、转化为工作站的脚本语言,再统一下发。因此会形成大量固定的 protocols。
|
||||
@@ -26,7 +30,7 @@
|
||||
|
||||
### 0.3 厂家开发的定制大型工站
|
||||
|
||||

|
||||

|
||||
|
||||
由厂家开发,具备完善的物料系统、任务系统甚至调度系统;由 PLC 或 OpenAPI TCP 协议统一通信
|
||||
|
||||
595
docs/developer_guide/networking_overview.md
Normal file
595
docs/developer_guide/networking_overview.md
Normal 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 云平台文档
|
||||
|
||||
Reference in New Issue
Block a user