Update docs

This commit is contained in:
Xuwznln
2025-11-18 17:17:43 +08:00
parent 653e6e1ac3
commit 6a681e1d73
121 changed files with 7700 additions and 985 deletions

View File

@@ -0,0 +1,207 @@
# 实例电池装配工站接入PLC控制
> **文档类型**:实际应用案例
> **适用场景**:使用 PLC 控制的电池装配工站接入
> **前置知识**{doc}`../add_device` | {doc}`../add_registry`
本指南以电池装配工站为实际案例,引导你完成 PLC 控制设备的完整接入流程,包括新建工站文件、编写驱动与寄存器读写、生成注册表、上传及注意事项。
## 案例概述
**设备类型**:电池装配工站
**通信方式**Modbus TCP (PLC)
**工站基类**`WorkstationBase`
**主要功能**:电池组装、寄存器读写、数据采集
## 1. 新建工站文件
### 1.1 创建工站文件
`unilabos/devices/workstation/coin_cell_assembly` 目录下新建工站文件,如 `coin_cell_assembly.py`。工站类需继承 `WorkstationBase`,并在构造函数中初始化通信客户端与寄存器映射。
```python
from typing import Optional
# 工站基类
from unilabos.devices.workstation.workstation_base import WorkstationBase
# Modbus 通讯与寄存器 CSV 支持
from unilabos.device_comms.modbus_plc.client import TCPClient, BaseClient
class CoinCellAssemblyWorkstation(WorkstationBase):
def __init__(
self,
station_resource,
address: str = "192.168.1.20",
port: str = "502",
*args,
**kwargs,
):
super().__init__(station_resource=station_resource, *args, **kwargs)
self.station_resource = station_resource # 物料台面Deck
self.success: bool = False
self.allow_data_read: bool = False
self.csv_export_thread = None
self.csv_export_running = False
self.csv_export_file: Optional[str] = None
# 连接 PLC并注册寄存器节点
tcp = TCPClient(addr=address, port=port)
tcp.client.connect()
self.nodes = BaseClient.load_csv(".../PLC_register.csv")
self.client = tcp.register_node_list(self.nodes)
```
## 2. 编写驱动与寄存器读写
### 2.1 寄存器示例
- `COIL_SYS_START_CMD`BOOL地址 8010启动命令脉冲式
- `COIL_SYS_START_STATUS`BOOL地址 8210启动状态
- `REG_DATA_OPEN_CIRCUIT_VOLTAGE`FLOAT32地址 10002开路电压
- `REG_DATA_ASSEMBLY_PRESSURE`INT16地址 10014压制扣电压力
### 2.2 最小驱动示例
```python
from unilabos.device_comms.modbus_plc.modbus import WorderOrder
def start_and_read_metrics(self):
# 1) 下发启动(置 True 再复位 False
self.client.use_node('COIL_SYS_START_CMD').write(True)
self.client.use_node('COIL_SYS_START_CMD').write(False)
# 2) 等待进入启动状态
while True:
status, _ = self.client.use_node('COIL_SYS_START_STATUS').read(1)
if bool(status[0]):
break
# 3) 读取关键数据FLOAT32 需读 2 个寄存器并指定字节序)
voltage, _ = self.client.use_node('REG_DATA_OPEN_CIRCUIT_VOLTAGE').read(
2, word_order=WorderOrder.LITTLE
)
pressure, _ = self.client.use_node('REG_DATA_ASSEMBLY_PRESSURE').read(1)
return {
'open_circuit_voltage': voltage,
'assembly_pressure': pressure,
}
```
> 提示:若需参数下发,可在 PLC 端设置标志寄存器并完成握手复位,避免粘连与竞争。
## 3. 本地生成注册表并校验
完成工站类与驱动后,需要生成(或更新)工站注册表供系统识别。
### 3.1 新增工站设备(或资源)首次生成注册表
首先通过以下命令启动unilab。进入unilab系统状态检查页面
```bash
python unilabos\app\main.py -g celljson.json --ak <user的AK> --sk <user的SK>
```
点击注册表编辑,进入注册表编辑页面
![系统状态页面](image_battery_plc/unilab_sys_status.png)
按照图示步骤填写自动生成注册表信息:
![注册表生成流程](image_battery_plc/unilab_registry_process.png)
步骤说明:
1. 选择新增的工站`coin_cell_assembly.py`文件
2. 点击分析按钮,分析`coin_cell_assembly.py`文件
3. 选择`coin_cell_assembly.py`文件中继承`WorkstationBase`
4. 填写新增的工站.py文件与`unilabos`目录的距离。例如,新增的工站文件`coin_cell_assembly.py`路径为`unilabos\devices\workstation\coin_cell_assembly\coin_cell_assembly.py`,则此处填写`unilabos.devices.workstation.coin_cell_assembly`
5. 此处填写新定义工站的类的名字(名称可以自拟)
6. 填写新的工站注册表备注信息
7. 生成注册表
以上操作步骤完成则会生成的新的注册表YAML文件如下图
![生成的YAML文件](image_battery_plc/unilab_new_yaml.png)
### 3.2 添加新生成注册表
`unilabos\registry\devices`目录下新建一个yaml文件此处新建文件命名为`coincellassemblyworkstation_device.yaml`,将上面生成的新的注册表信息粘贴到`coincellassemblyworkstation_device.yaml`文件中。
在终端输入以下命令进行注册表补全操作。
```bash
python unilabos\app\register.py --complete_registry
```
### 3.3 启动并上传注册表
新增设备之后启动unilab需要增加`--upload_registry`参数,来上传注册表信息。
```bash
python unilabos\app\main.py -g celljson.json --ak <user的AK> --sk <user的SK> --upload_registry
```
## 4. 注意事项
### 4.1 验证模块路径
在新生成的 YAML 中,确认 `module` 指向新工站类。本例中需检查 `coincellassemblyworkstation_device.yaml` 文件中是否正确指向了 `CoinCellAssemblyWorkstation` 类:
```yaml
module: unilabos.devices.workstation.coin_cell_assembly.coin_cell_assembly:CoinCellAssemblyWorkstation
```
### 4.2 首次接入流程
首次新增设备(或资源)需要完整流程:
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 设备接入流程,可以作为其他类似设备接入的参考模板。

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -0,0 +1,409 @@
# 实例:物料构建指南
> **文档类型**:物料系统实战指南
> **适用场景**工作站物料系统构建、Deck/Warehouse/Carrier/Bottle 配置
> **前置知识**PyLabRobot 基础 | 资源管理概念
## 概述
在UniLab-OS系统中任何工作站中所需要用到的物料主要包括四个核心组件
1. **桌子Deck** - 工作台面,定义整个工作空间的布局
2. **堆栈Warehouse** - 存储区域,用于放置载具和物料
3. **载具Carriers** - 承载瓶子等物料的容器架
4. **瓶子Bottles** - 实际的物料容器
本文档以BioYond工作站为例详细说明如何构建这些物料组件。
## 文件结构
物料定义文件位于 `unilabos/resources/` 文件夹中:
```
unilabos/resources/bioyond/
├── decks.py # 桌子定义
├── YB_warehouses.py # 堆栈定义
├── YB_bottle_carriers.py # 载具定义
└── YB_bottles.py # 瓶子定义
```
对应的注册表文件位于 `unilabos/registry/resources/bioyond/` 文件夹中:
```
unilabos/registry/resources/bioyond/
├── deck.yaml # 桌子注册表
├── YB_bottle_carriers.yaml # 载具注册表
└── YB_bottle.yaml # 瓶子注册表
```
## 1. 桌子Deck构建
桌子是整个工作站的基础,定义了工作空间的尺寸和各个组件的位置。
### 代码示例 (decks.py)
```python
from pylabrobot.resources import Coordinate, Deck
from unilabos.resources.bioyond.YB_warehouses import (
bioyond_warehouse_2x2x1,
bioyond_warehouse_3x5x1,
bioyond_warehouse_20x1x1,
bioyond_warehouse_3x3x1,
bioyond_warehouse_10x1x1
)
class BIOYOND_YB_Deck(Deck):
def __init__(
self,
name: str = "YB_Deck",
size_x: float = 4150, # 桌子X方向尺寸 (mm)
size_y: float = 1400.0, # 桌子Y方向尺寸 (mm)
size_z: float = 2670.0, # 桌子Z方向尺寸 (mm)
category: str = "deck",
setup: bool = False
) -> None:
super().__init__(name=name, size_x=4150.0, size_y=1400.0, size_z=2670.0)
if setup:
self.setup() # 当在工作站配置中setup为True时自动创建并放置所有预定义的堆栈
def setup(self) -> None:
# 定义桌子上的各个仓库区域
self.warehouses = {
"自动堆栈-左": bioyond_warehouse_2x2x1("自动堆栈-左"),
"自动堆栈-右": bioyond_warehouse_2x2x1("自动堆栈-右"),
"手动堆栈-左": bioyond_warehouse_3x5x1("手动堆栈-左"),
"手动堆栈-右": bioyond_warehouse_3x5x1("手动堆栈-右"),
"粉末加样头堆栈": bioyond_warehouse_20x1x1("粉末加样头堆栈"),
"配液站内试剂仓库": bioyond_warehouse_3x3x1("配液站内试剂仓库"),
"试剂替换仓库": bioyond_warehouse_10x1x1("试剂替换仓库"),
}
# 定义各个仓库在桌子上的坐标位置
self.warehouse_locations = {
"自动堆栈-左": Coordinate(-100.3, 171.5, 0.0),
"自动堆栈-右": Coordinate(3960.1, 155.9, 0.0),
"手动堆栈-左": Coordinate(-213.3, 804.4, 0.0),
"手动堆栈-右": Coordinate(3960.1, 807.6, 0.0),
"粉末加样头堆栈": Coordinate(415.0, 1301.0, 0.0),
"配液站内试剂仓库": Coordinate(2162.0, 437.0, 0.0),
"试剂替换仓库": Coordinate(1173.0, 802.0, 0.0),
}
# 将仓库分配到桌子的指定位置
for warehouse_name, warehouse in self.warehouses.items():
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
```
### 在工作站配置中的使用
当在工作站配置文件中定义桌子时,可以通过`setup`参数控制是否自动建立所有堆栈:
```json
{
"id": "YB_Bioyond_Deck",
"name": "YB_Bioyond_Deck",
"children": [],
"parent": "bioyond_cell_workstation",
"type": "deck",
"class": "BIOYOND_YB_Deck",
"config": {
"type": "BIOYOND_YB_Deck",
"setup": true
},
"data": {}
}
```
**重要说明**
-`"setup": true` 时,系统会自动调用桌子的 `setup()` 方法
- 这将创建并放置所有预定义的堆栈到桌子上的指定位置
- 如果 `"setup": false` 或省略该参数,则只创建空桌子,需要手动添加堆栈
### 关键要点注释
- `size_x`, `size_y`, `size_z`: 定义桌子的物理尺寸
- `warehouses`: 字典类型,包含桌子上所有的仓库区域
- `warehouse_locations`: 定义每个仓库在桌子坐标系中的位置
- `assign_child_resource()`: 将仓库资源分配到桌子的指定位置
- `setup()`: 可选的自动设置方法,初始化时可调用
## 2. 堆栈Warehouse构建
堆栈定义了存储区域的规格和布局,用于放置载具。
### 代码示例 (YB_warehouses.py)
```python
from unilabos.resources.warehouse import WareHouse, YB_warehouse_factory
def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
"""创建BioYond 1x4x4仓库
Args:
name: 仓库名称
Returns:
WareHouse: 仓库对象
"""
return YB_warehouse_factory(
name=name,
num_items_x=1, # X方向位置数量
num_items_y=4, # Y方向位置数量
num_items_z=4, # Z方向位置数量层数
dx=10.0, # X方向起始偏移
dy=10.0, # Y方向起始偏移
dz=10.0, # Z方向起始偏移
item_dx=137.0, # X方向间距
item_dy=96.0, # Y方向间距
item_dz=120.0, # Z方向间距层高
category="warehouse",
)
def bioyond_warehouse_2x2x1(name: str) -> WareHouse:
"""创建BioYond 2x2x1仓库自动堆栈"""
return YB_warehouse_factory(
name=name,
num_items_x=2,
num_items_y=2,
num_items_z=1, # 单层
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="YB_warehouse",
)
```
### 关键要点注释
- `num_items_x/y/z`: 定义仓库在各个方向的位置数量
- `dx/dy/dz`: 第一个位置的起始偏移坐标
- `item_dx/dy/dz`: 相邻位置之间的间距
- `category`: 仓库类别,用于分类管理
- `YB_warehouse_factory`: 统一的仓库创建工厂函数
## 3. 载具Carriers构建
载具是承载瓶子的容器架,定义了瓶子的排列方式和位置。
### 代码示例 (YB_bottle_carriers.py)
```python
from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder, create_ordered_items_2d
from unilabos.resources.itemized_carrier import Bottle, BottleCarrier
from unilabos.resources.bioyond.YB_bottles import YB_pei_ye_xiao_Bottle
def YB_peiyepingxiaoban(name: str) -> BottleCarrier:
"""配液瓶(小)板 - 4x2布局8个位置
Args:
name: 载具名称
Returns:
BottleCarrier: 载具对象包含8个配液瓶位置
"""
# 载具物理尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 65.0
# 瓶位参数
bottle_diameter = 35.0 # 瓶子直径
bottle_spacing_x = 42.0 # X方向瓶子间距
bottle_spacing_y = 35.0 # Y方向瓶子间距
# 计算起始位置 (居中排列)
start_x = (carrier_size_x - (4 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
# 创建瓶位布局4列x2行
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=4, # 4列
num_items_y=2, # 2行
dx=start_x,
dy=start_y,
dz=5.0, # 瓶子底部高度
item_dx=bottle_spacing_x,
item_dy=bottle_spacing_y,
size_x=bottle_diameter,
size_y=bottle_diameter,
size_z=carrier_size_z,
)
# 为每个瓶位设置名称
for k, v in sites.items():
v.name = f"{name}_{v.name}"
# 创建载具对象
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=sites,
model="YB_peiyepingxiaoban",
)
# 设置载具布局参数
carrier.num_items_x = 4
carrier.num_items_y = 2
carrier.num_items_z = 1
# 定义瓶子排列顺序
ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"]
# 为每个位置创建瓶子实例
for i in range(8):
carrier[i] = YB_pei_ye_xiao_Bottle(f"{name}_bottle_{ordering[i]}")
return carrier
```
### 关键要点注释
- `carrier_size_x/y/z`: 载具的物理尺寸
- `bottle_diameter`: 瓶子的直径,用于计算瓶位大小
- `bottle_spacing_x/y`: 瓶子之间的间距
- `create_ordered_items_2d`: 创建二维排列的瓶位
- `sites`: 瓶位字典,存储所有瓶子位置信息
- `ordering`: 定义瓶位的命名规则如A1, A2, B1等
## 4. 瓶子Bottles构建
瓶子是最终的物料容器,定义了容器的物理属性。
### 代码示例 (YB_bottles.py)
```python
from unilabos.resources.itemized_carrier import Bottle
def YB_pei_ye_xiao_Bottle(
name: str,
diameter: float = 35.0, # 瓶子直径 (mm)
height: float = 60.0, # 瓶子高度 (mm)
max_volume: float = 30000.0, # 最大容量 (μL) - 30mL
barcode: str = None, # 条码
) -> Bottle:
"""创建配液瓶(小)
Args:
name: 瓶子名称
diameter: 瓶子直径
height: 瓶子高度
max_volume: 最大容量(微升)
barcode: 条码标识
Returns:
Bottle: 瓶子对象
"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_pei_ye_xiao_Bottle",
)
def YB_ye_Bottle(
name: str,
diameter: float = 40.0,
height: float = 70.0,
max_volume: float = 50000.0, # 最大容量
barcode: str = None,
) -> Bottle:
"""创建液体瓶"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_ye_Bottle",
)
```
### 关键要点注释
- `diameter`: 瓶子直径,影响瓶位大小计算
- `height`: 瓶子高度,用于碰撞检测和移液计算
- `max_volume`: 最大容量单位为微升μL
- `barcode`: 条码标识,用于瓶子追踪
- `model`: 型号标识,用于区分不同类型的瓶子
## 5. 注册表配置
创建完物料定义后,需要在注册表中注册这些物料,使系统能够识别和使用它们。
`unilabos/registry/resources/bioyond/` 目录下创建:
- `deck.yaml` - 桌子注册表
- `YB_bottle_carriers.yaml` - 载具注册表
- `YB_bottle.yaml` - 瓶子注册表
### 5.1 桌子注册表 (deck.yaml)
```yaml
BIOYOND_YB_Deck:
category:
- deck # 前端显示的分类存放
class:
module: unilabos.resources.bioyond.decks:BIOYOND_YB_Deck # 定义桌子的类的路径
type: pylabrobot
description: BIOYOND_YB_Deck # 描述信息
handles: []
icon: 配液站.webp # 图标文件
init_param_schema: {}
registry_type: resource # 注册类型
version: 1.0.0 # 版本号
```
### 5.2 载具注册表 (YB_bottle_carriers.yaml)
```yaml
YB_peiyepingxiaoban:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_peiyepingxiaoban
type: pylabrobot
description: YB_peiyepingxiaoban
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
```
### 5.3 瓶子注册表 (YB_bottle.yaml)
```yaml
YB_pei_ye_xiao_Bottle:
category:
- yb3
- YB_bottle
class:
module: unilabos.resources.bioyond.YB_bottles:YB_pei_ye_xiao_Bottle
type: pylabrobot
description: YB_pei_ye_xiao_Bottle
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
```
### 注册表关键要点注释
- `category`: 物料分类,用于在云端(网页界面)中的分类中显示
- `module`: Python模块路径格式为 `模块路径:类名`
- `type`: 框架类型,通常为 `pylabrobot`(默认即可)
- `description`: 描述信息,显示在用户界面中
- `icon`: (名称唯一自动匹配后端上传的图标文件名,显示在云端)
- `registry_type`: 固定为 `resource`
- `version`: 版本号,用于版本管理

View File

@@ -0,0 +1,413 @@
# 实例物料教程Resource
> **文档类型**:物料系统完整教程
> **适用场景**:物料格式转换、多系统物料对接、资源结构理解
> **前置知识**Python 基础 | JSON 数据结构
本教程面向 Uni-Lab-OS 的开发者,讲解"物料"的核心概念、3种物料格式UniLab、PyLabRobot、奔耀Bioyond及其相互转换方法并说明4种 children 结构表现形式及使用场景。
---
## 1. 物料是什么
- **物料Resource**指实验工作站中的实体对象包括设备device、操作甲板 deck、试剂、实验耗材也包括设备上承载的具体物料或者包含的容器如container/plate/well/瓶/孔/片等)。
- **物料基本信息**(以 UniLab list格式为例
```jsonc
{
"id": "plate", // 某一类物料的唯一名称
"name": "50ml瓶装试剂托盘", // 在云端显示的名称
"sample_id": null, // 同类物料的不同样品
"children": [
"50ml试剂瓶" // 表示托盘上有一个 50ml 试剂瓶
],
"parent": "deck", // 此物料放置在 deck 上
"type": "plate", // 物料类型
"class": "plate", // 物料对应的注册/类名
"position": {
"x": 0, // 初始放置位置
"y": 0,
"z": 0
},
"config": { // 固有配置(尺寸、旋转等)
"size_x": 400.0,
"size_y": 400.0,
"size_z": 400.0,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
}
},
"data": {
"bottle_number": 1 // 动态数据(可变化)
}
}
```
## 2. 3种物料格式概览(UniLab、PyLabRobot、奔耀Bioyond)
### 2.1 UniLab 物料格式(云端/项目内通用)
- 结构特征:顶层通常是 `nodes` 列表;每个节点是扁平字典,`children` 是子节点 `id` 列表;`parent` 为父节点 `id``null`
- 用途:
- 云端数据存储、前端可视化、与图结构算法互操作
- 在上传/下载/部署配置时作为标准交换格式
示例片段UniLab 物料格式):
```jsonc
{
"nodes": [
{
"id": "a",
"name": "name_a",
"sample_id": 1,
"type": "deck",
"class": "deck",
"parent": null,
"children": ["b1"],
"position": {"x": 0, "y": 0, "z": 0},
"config": {},
"data": {}
},
{
"id": "b1",
"name": "name_b1",
"sample_id": 1,
"type": "plate",
"class": "plate",
"parent": "a1",
"children": [],
"position": {"x": 0, "y": 0, "z": 0},
"config": {},
"data": {}
}
]
}
```
### 2.2 PyLabRobotPLR物料格式实验流程运行时
- 结构特征:严格的层级树,`children` 为“子资源字典列表”(每个子节点本身是完整对象)。
- 用途:
- 实验流程执行与调度PLR 运行时期望的资源对象格式
- 通过 `Resource.deserialize/serialize``load_all_state/serialize_all_state` 与对象交互
示例片段PRL 物料格式)::
```json
{
"name": "deck",
"type": "Deck",
"category": "deck",
"location": {"x": 0, "y": 0, "z": 0, "type": "Coordinate"},
"rotation": {"x": 0, "y": 0, "z": 0, "type": "Rotation"},
"parent_name": null,
"children": [
{
"name": "plate_1",
"type": "Plate",
"category": "plate_96",
"location": {"x": 100, "y": 0, "z": 0, "type": "Coordinate"},
"rotation": {"x": 0, "y": 0, "z": 0, "type": "Rotation"},
"parent_name": "deck",
"children": [
{
"name": "A1",
"type": "Well",
"category": "well",
"location": {"x": 0, "y": 0, "z": 0, "type": "Coordinate"},
"rotation": {"x": 0, "y": 0, "z": 0, "type": "Rotation"},
"parent_name": "plate_1",
"children": []
}
]
}
]
}
```
### 2.3 奔耀 Bioyond 物料格式(第三方来源)
一般是厂商自己定义的json格式和字段信息需要提取和对应。以下为示例说明。
- 结构特征:顶层 `data` 列表,每项包含 `typeName``code``barCode``name``quantity``unit``locations`(仓位 `whName``x/y/z`)、`detail`(细粒度内容,如瓶内液体或孔位物料)。
- 用途:
- 第三方 WMS/设备的物料清单输入
- 需要自定义映射表将 `typeName` → PLR 类名,对 `locations`/`detail` 进行落位/赋值
示例片段奔耀Bioyond 物料格式):
```json
{
"data": [
{
"id": "3a1b5c10-d4f3-01ac-1e64-5b4be2add4b1",
"typeName": "液",
"code": "0006-00014",
"barCode": "",
"name": "EMC",
"quantity": 50,
"lockQuantity": 2.057,
"unit": "瓶",
"status": 1,
"isUse": false,
"locations": [
{
"id": "3a19da43-57b5-5e75-552f-8dbd0ad1075f",
"whid": "3a19da43-57b4-a2a8-3f52-91dbbeb836db",
"whName": "配液站内试剂仓库",
"code": "0003-0003",
"x": 1,
"y": 3,
"z": 1,
"quantity": 0
}
],
"detail": [
{
"code": "0006-00014-01",
"name": "EMC-瓶-1",
"x": 1,
"y": 3,
"z": 1,
"quantity": 500.0
}
]
}
],
"code": 1,
"message": "",
"timestamp": 0
}
```
### 2.4 3种物料格式关键字段对应(UniLab、PyLabRobot、奔耀Bioyond)
| 含义 | UniLab | PyLabRobot (PLR) | 奔耀 Bioyond |
| - | - | - | - |
| 节点唯一名 | `id` | `name` | `name` |
| 父节点引用 | `parent` | `parent_name` | `locations` 坐标(无直接父名,需映射坐标下的物料) |
| 子节点集合 | `children`id 列表或对象列表,视结构而定) | `children`(对象列表) | `detail`(明细,非严格树结构,需要自定义映射) |
| 类型(抽象类别) | `type`device/container/plate/deck/…) | `category`plate/well/…),以及类名 `type` | `typeName`(厂商自定义,如“液”、“加样头(大)”) |
| 运行/业务数据 | `data` | 通过 `serialize_all_state()`/`load_all_state()` 管理的状态 | `quantity``lockQuantity` 等业务数值 |
| 固有配置 | `config`size_x/size_y/size_z/model/ordering… | 资源字典中的同名键(反序列化时按构造签名取用) | 厂商自定义字段(需映射入 PLR/UniLab 的 `config``data` |
| 空间位置 | `position`x/y/z | `location`Coordinate + `rotation`Rotation | `locations`whName、x/y/z不含旋转 |
| 条码/标识 | `config.barcode`(可选) | 常放在配置键中(如 `barcode` | `barCode` |
| 数量单位 | 无固定键,通常在 `data` | 无固定键,通常在配置或状态中 | `unit` |
| 物料编码 | 通常在 `config``data` 自定义 | 通常在配置中自定义 | `code` |
说明:
- Bioyond 不提供显式的树形父子关系,通常通过 `locations` 将物料落位到某仓位/坐标。用 `detail` 表示子级明细。
---
## 3. children 的四种结构表示
- **list扁平列表**:每个节点是扁平字典,`children` 为子节点 `id` 数组。示例UniLab `nodes` 中的单个节点。
```json
{
"nodes": [
{ "id": "root", "parent": null, "children": ["child1"] },
{ "id": "child1", "parent": "root", "children": [] }
]
}
```
- **dict嵌套字典**:节点的 `children``{ child_id: child_node_dict }` 字典。
```json
{
"id": "root",
"parent": null,
"children": {
"child1": { "id": "child1", "parent": "root", "children": {} }
}
}
```
- **tree树形列表**:顶层是 `[root_node, ...]`,每个 `node.children` 是“子节点对象列表”(而非 id 列表)。
```json
[
{
"id": "root",
"parent": null,
"children": [
{ "id": "child1", "parent": "root", "children": [] }
]
}
]
```
- **nestdict顶层嵌套字典**:顶层是 `{root_id: root_node, ...}`,或者根节点自身带 `children: {id: node}` 形态。
```json
{
"root": {
"id": "root",
"parent": null,
"children": {
"child1": { "id": "child1", "parent": "root", "children": {} }
}
}
}
```
这些结构之间可使用 `graphio.py` 中的工具函数互转(见下一节)。
---
## 4. 转换函数及调用
核心代码文件:`unilabos/resources/graphio.py`
### 4.1 结构互转list/dict/tree/nestdict
代码引用:
```217:239:unilabos/resources/graphio.py
def dict_to_tree(nodes: dict, devices_only: bool = False) -> list[dict]:
# ... 由扁平 dictid->node生成树children 为对象列表)
```
```241:267:unilabos/resources/graphio.py
def dict_to_nested_dict(nodes: dict, devices_only: bool = False) -> dict:
# ... 由扁平 dict 生成嵌套字典children 为 {id:node}
```
```270:273:unilabos/resources/graphio.py
def list_to_nested_dict(nodes: list[dict]) -> dict:
# ... 由扁平列表children 为 id 列表)转嵌套字典
```
```275:286:unilabos/resources/graphio.py
def tree_to_list(tree: list[dict]) -> list[dict]:
# ... 由树形列表转回扁平列表children 还原为 id 列表)
```
```289:337:unilabos/resources/graphio.py
def nested_dict_to_list(nested_dict: dict) -> list[dict]:
# ... 由嵌套字典转回扁平列表
```
常见路径:
- UniLab 扁平列表 → 树:`dict_to_tree({r["id"]: r for r in resources})`
- 树 → UniLab 扁平列表:`tree_to_list(resources_tree)`
- 扁平列表 ↔ 嵌套字典:`list_to_nested_dict` / `nested_dict_to_list`
### 4.2 UniLab ↔ PyLabRobotPLR
高层封装:
```339:368:unilabos/resources/graphio.py
def convert_resources_to_type(resources_list: list[dict], resource_type: Union[type, list[type]], *, plr_model: bool = False):
# UniLab -> (NestedDict or PLR)
```
```371:395:unilabos/resources/graphio.py
def convert_resources_from_type(resources_list, resource_type: Union[type, list[type]], *, is_plr: bool = False):
# (NestedDict or PLR) -> UniLab 扁平列表
```
底层转换:
```398:441:unilabos/resources/graphio.py
def resource_ulab_to_plr(resource: dict, plr_model=False) -> "ResourcePLR":
# UniLab 单节点(树根) -> PLR Resource 对象
```
```443:481:unilabos/resources/graphio.py
def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None, with_children=True):
# PLR Resource -> UniLab 单节点(dict)
```
示例:
```python
from unilabos.resources.graphio import convert_resources_to_type, convert_resources_from_type
from pylabrobot.resources.resource import Resource as ResourcePLR
# UniLab 扁平列表 -> PLR 根资源对象
plr_root = convert_resources_to_type(resources_list=ulab_list, resource_type=ResourcePLR)
# PLR 资源对象 -> UniLab 扁平列表(用于保存/上传)
ulab_flat = convert_resources_from_type(resources_list=plr_root, resource_type=ResourcePLR)
```
可选项:
- `plr_model=True`:保留 `model` 字段(默认会移除)。
- `with_children=False``resource_plr_to_ulab` 仅转换当前节点。
### 4.3 奔耀Bioyond→ PLR及进一步到 UniLab
转换入口:
```483:527:unilabos/resources/graphio.py
def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: dict = {}, deck: Any = None) -> list[dict]:
# Bioyond 列表 -> PLR 资源列表,并可根据 deck.warehouses 将资源落位
```
使用示例:
```python
import json
from unilabos.resources.graphio import resource_bioyond_to_plr, convert_resources_from_type
from pylabrobot.resources.resource import Resource as ResourcePLR
resp = json.load(open("unilabos/devices/workstation/bioyond_cell/bioyond_test_yibin.json", encoding="utf-8"))
materials = resp["data"]
# 将第三方类型name映射到 PLR 资源类名(需根据现场定义)
type_mapping = {
"液": "RegularContainer",
"加样头(大)": "RegularContainer"
}
plr_list = resource_bioyond_to_plr(materials, type_mapping=type_mapping, deck=None)
# 如需上传云端UniLab 扁平格式):
ulab_flat = convert_resources_from_type(plr_list, [ResourcePLR])
```
说明:
- `type_mapping` 必须由开发者根据设备/物料种类人工维护。
- 如传入 `deck`,且 `deck.warehouses` 命名与 `whName` 对应可将物料安放到仓库坐标x/y/z
---
## 5. 何时使用哪种格式
- **云端/持久化**:使用 UniLab 物料格式(扁平 `nodes` 列表children 为 id 列表)。便于版本化、可视化与网络传输。
- **实验工作流执行**:使用 PyLabRobotPLR格式。PLR 运行时依赖严格的树形资源结构与对象 API。
- **第三方设备/系统Bioyond输入**:保持来源格式不变,使用 `resource_bioyond_to_plr` + 人工 `type_mapping` 将其转换为 PLR必要时再转 UniLab
---
## 6. 常见问题与注意事项
- **children 形态不一致**:不同函数期望不同 children 形态,注意在进入转换前先用“结构互转”工具函数标准化形态。
- **devices_only**`dict_to_tree/dict_to_nested_dict` 支持仅保留 `type == device` 的节点。
- **模型/类型字段**PLR 对象序列化参数有所差异,`resource_ulab_to_plr` 内部会根据构造签名移除不兼容字段(如 `category`)。
- **驱动初始化**`initialize_resource(s)` 支持从注册表/类路径创建 PLR/UniLab 资源或列表。
参考代码:
```530:577:unilabos/resources/graphio.py
def initialize_resource(resource_config: dict, resource_type: Any = None) -> Union[list[dict], ResourcePLR]:
# 从注册类/模块反射创建资源,或将 UniLab 字典包装为列表
```
```580:597:unilabos/resources/graphio.py
def initialize_resources(resources_config) -> list[dict]:
# 批量初始化
```

View File

@@ -0,0 +1,782 @@
# 实例:工作站模板架构设计与对接指南
> **文档类型**:架构设计指南与实战案例
> **适用场景**:大型工作站接入、子设备管理、物料系统集成
> **前置知识**{doc}`../add_device` | {doc}`../add_registry`
## 0. 问题简介
我们可以从以下几类例子,来理解对接大型工作站需要哪些设计。本文档之后的实战案例也将由这些组成。
### 0.1 自研常量有机工站:最重要的是子设备管理和通信转发
![workstation_organic_yed](../image/workstation_architecture/workstation_organic_yed.png)
![workstation_organic](../image/workstation_architecture/workstation_organic.png)
这类工站由开发者自研,组合所有子设备和实验耗材、希望让他们在工作站这一级协调配合;
1. 工作站包含大量已经注册的子设备可能各自通信组态很不相同部分设备可能会拥有同一个通信设备作为出口如2个泵共用1个串口、所有设备共同接入PLC等。
2. 任务系统是统一实现的 protocolsprotocols 中会将高层指令处理成各子设备配合的工作流 json并管理执行、同时更改物料信息
3. 物料系统较为简单直接,如常量有机化学仅为工作站内固定的瓶子,初始化时就已固定;随后在任务执行过程中,记录试剂量更改信息
### 0.2 移液工作站:物料系统和工作流模板管理
![workstation_liquid_handler](../image/workstation_architecture/workstation_liquid_handler.png)
1. 绝大多数情况没有子设备,有时候选配恒温震荡等模块时,接口也由工作站提供
2. 所有任务系统均由工作站本身实现并下发指令有统一的抽象函数可实现pick_up_tips, aspirate, dispense, transfer 等)。有时需要将这些指令组合、转化为工作站的脚本语言,再统一下发。因此会形成大量固定的 protocols。
3. 物料系统为固定的板位系统:台面上有多个可摆放位置,摆放标准孔板。
### 0.3 厂家开发的定制大型工站
![workstation_by_supplier](../image/workstation_architecture/workstation_by_supplier.png)
由厂家开发,具备完善的物料系统、任务系统甚至调度系统;由 PLC 或 OpenAPI TCP 协议统一通信
1. 在监控状态时,希望展现子设备的状态;但子设备仅为逻辑概念,通信由工作站上位机接口提供;部分情况下,子设备状态是被记录在文件中的,需要读取
2. 工作站有自己的工作流系统甚至调度系统;可以通过脚本/PLC连续读写来配置工作站可用的工作流
3. 部分拥有完善的物料入库、出库、过程记录,需要与 Uni-Lab-OS 物料系统对接
## 1. 整体架构图
### 1.1 工作站核心架构
```{mermaid}
graph TB
subgraph "工作站模板组成"
WB[WorkstationBase<br/>工作流状态管理]
RPN[ROS2WorkstationNode<br/>Protocol执行引擎]
WB -.post_init关联.-> RPN
end
subgraph "物料管理系统"
DECK[Deck<br/>PLR本地物料系统]
RS[ResourceSynchronizer<br/>外部物料同步器]
WB --> DECK
WB --> RS
RS --> DECK
end
subgraph "通信与子设备管理"
HW[hardware_interface<br/>硬件通信接口]
SUBDEV[子设备集合<br/>pumps/grippers/sensors]
WB --> HW
RPN --> SUBDEV
HW -.代理模式.-> RPN
end
subgraph "工作流任务系统"
PROTO[Protocol定义<br/>LiquidHandling/PlateHandling]
WORKFLOW[Workflow执行器<br/>步骤管理与编排]
RPN --> PROTO
RPN --> WORKFLOW
WORKFLOW --> SUBDEV
end
```
### 1.2 外部系统对接关系
```{mermaid}
graph LR
subgraph "Uni-Lab-OS工作站"
WS[WorkstationBase + ROS2WorkstationNode]
DECK2[物料系统<br/>Deck]
HW2[通信接口<br/>hardware_interface]
HTTP[HTTP服务<br/>WorkstationHTTPService]
end
subgraph "外部物料系统"
BIOYOND[Bioyond物料管理]
LIMS[LIMS系统]
WAREHOUSE[第三方仓储]
end
subgraph "外部硬件系统"
PLC[PLC设备]
SERIAL[串口设备]
ROBOT[机械臂/机器人]
end
subgraph "云端系统"
CLOUD[UniLab云端<br/>资源管理]
MONITOR[监控与调度]
end
BIOYOND <-->|RPC双向同步| DECK2
LIMS -->|HTTP报送| HTTP
WAREHOUSE <-->|API对接| DECK2
PLC <-->|Modbus TCP| HW2
SERIAL <-->|串口通信| HW2
ROBOT <-->|SDK/API| HW2
WS -->|ROS消息| CLOUD
CLOUD -->|任务下发| WS
MONITOR -->|状态查询| WS
```
### 1.3 具体实现示例
```{mermaid}
graph TB
subgraph "工作站基类"
BASE[WorkstationBase<br/>抽象基类]
end
subgraph "Bioyond集成工作站"
BW[BioyondWorkstation]
BW_DECK[Deck + Warehouses]
BW_SYNC[BioyondResourceSynchronizer]
BW_HW[BioyondV1RPC]
BW_HTTP[HTTP报送服务]
BW --> BW_DECK
BW --> BW_SYNC
BW --> BW_HW
BW --> BW_HTTP
end
subgraph "纯协议节点"
PN[ProtocolNode]
PN_SUB[子设备集合]
PN_PROTO[Protocol工作流]
PN --> PN_SUB
PN --> PN_PROTO
end
subgraph "PLC控制工作站"
PW[PLCWorkstation]
PW_DECK[Deck物料系统]
PW_PLC[Modbus PLC客户端]
PW_WF[工作流定义]
PW --> PW_DECK
PW --> PW_PLC
PW --> PW_WF
end
BASE -.继承.-> BW
BASE -.继承.-> PN
BASE -.继承.-> PW
```
## 2. 类关系图
```{mermaid}
classDiagram
class WorkstationBase {
<<abstract>>
+_ros_node: ROS2WorkstationNode
+deck: Deck
+plr_resources: Dict[str, PLRResource]
+resource_synchronizer: ResourceSynchronizer
+hardware_interface: Union[Any, str]
+current_workflow_status: WorkflowStatus
+supported_workflows: Dict[str, WorkflowInfo]
+post_init(ros_node)*
+set_hardware_interface(interface)
+call_device_method(method, *args, **kwargs)
+get_device_status()
+is_device_available()
+get_deck()
+get_all_resources()
+find_resource_by_name(name)
+find_resources_by_type(type)
+sync_with_external_system()
+execute_workflow(name, params)
+stop_workflow(emergency)
+workflow_status
+is_busy
}
class ROS2WorkstationNode {
+device_id: str
+children: Dict[str, Any]
+sub_devices: Dict
+protocol_names: List[str]
+_action_clients: Dict
+_action_servers: Dict
+resource_tracker: DeviceNodeResourceTracker
+initialize_device(device_id, config)
+create_ros_action_server(action_name, mapping)
+execute_single_action(device_id, action, kwargs)
+update_resource(resources)
+transfer_resource_to_another(resources, target, sites)
+_setup_hardware_proxy(device, comm_device, read, write)
}
%% 物料管理相关类
class Deck {
+name: str
+children: List
+assign_child_resource()
}
class ResourceSynchronizer {
<<abstract>>
+workstation: WorkstationBase
+sync_from_external()*
+sync_to_external(plr_resource)*
+handle_external_change(change_info)*
}
class BioyondResourceSynchronizer {
+bioyond_api_client: BioyondV1RPC
+sync_interval: int
+last_sync_time: float
+initialize()
+sync_from_external()
+sync_to_external(resource)
+handle_external_change(change_info)
}
%% 硬件接口相关类
class HardwareInterface {
<<interface>>
}
class BioyondV1RPC {
+base_url: str
+api_key: str
+stock_material()
+add_material()
+material_inbound()
}
%% 服务类
class WorkstationHTTPService {
+workstation: WorkstationBase
+host: str
+port: int
+server: HTTPServer
+running: bool
+start()
+stop()
+_handle_step_finish_report()
+_handle_sample_finish_report()
+_handle_order_finish_report()
+_handle_material_change_report()
+_handle_error_handling_report()
}
%% 具体实现类
class BioyondWorkstation {
+bioyond_config: Dict
+workflow_mappings: Dict
+workflow_sequence: List
+post_init(ros_node)
+transfer_resource_to_another()
+resource_tree_add(resources)
+append_to_workflow_sequence(name)
+get_all_workflows()
+get_bioyond_status()
}
class ProtocolNode {
+post_init(ros_node)
}
%% 核心关系
WorkstationBase o-- ROS2WorkstationNode : post_init关联
WorkstationBase o-- WorkstationHTTPService : 可选服务
%% 物料管理侧
WorkstationBase *-- Deck : deck
WorkstationBase *-- ResourceSynchronizer : 可选组合
ResourceSynchronizer <|-- BioyondResourceSynchronizer
%% 硬件接口侧
WorkstationBase o-- HardwareInterface : hardware_interface
HardwareInterface <|.. BioyondV1RPC : 实现
BioyondResourceSynchronizer --> BioyondV1RPC : 使用
%% 继承关系
BioyondWorkstation --|> WorkstationBase
ProtocolNode --|> WorkstationBase
ROS2WorkstationNode --|> BaseROS2DeviceNode : 继承
```
## 3. 工作站启动时序图
```{mermaid}
sequenceDiagram
participant APP as Application
participant WS as WorkstationBase
participant DECK as PLR Deck
participant SYNC as ResourceSynchronizer
participant HW as HardwareInterface
participant ROS as ROS2WorkstationNode
participant HTTP as HTTPService
APP->>WS: 创建工作站实例(__init__)
WS->>DECK: 初始化PLR Deck
DECK->>DECK: 创建Warehouse等子资源
DECK-->>WS: Deck创建完成
WS->>HW: 创建硬件接口(如BioyondV1RPC)
HW->>HW: 建立连接(PLC/RPC/串口等)
HW-->>WS: 硬件接口就绪
WS->>SYNC: 创建ResourceSynchronizer(可选)
SYNC->>HW: 使用hardware_interface
SYNC->>SYNC: 初始化同步配置
SYNC-->>WS: 同步器创建完成
WS->>SYNC: sync_from_external()
SYNC->>HW: 查询外部物料系统
HW-->>SYNC: 返回物料数据
SYNC->>DECK: 转换并添加到Deck
SYNC-->>WS: 同步完成
Note over WS: __init__完成,等待ROS节点
APP->>ROS: 初始化ROS2WorkstationNode
ROS->>ROS: 初始化子设备(children)
ROS->>ROS: 创建Action客户端
ROS->>ROS: 设置硬件接口代理
ROS-->>APP: ROS节点就绪
APP->>WS: post_init(ros_node)
WS->>WS: self._ros_node = ros_node
WS->>ROS: update_resource([deck])
ROS->>ROS: 上传物料到云端
ROS-->>WS: 上传完成
WS->>HTTP: 创建WorkstationHTTPService(可选)
HTTP->>HTTP: 启动HTTP服务器线程
HTTP-->>WS: HTTP服务启动
WS-->>APP: 工作站完全就绪
```
## 4. 工作流执行时序图Protocol模式
```{mermaid}
sequenceDiagram
participant CLIENT as 客户端
participant ROS as ROS2WorkstationNode
participant WS as WorkstationBase
participant HW as HardwareInterface
participant DECK as PLR Deck
participant CLOUD as 云端资源管理
participant DEV as 子设备
CLIENT->>ROS: 发送Protocol Action请求
ROS->>ROS: execute_protocol回调
ROS->>ROS: 从Goal提取参数
ROS->>ROS: 调用protocol_steps_generator
ROS->>ROS: 生成action步骤列表
ROS->>WS: 更新workflow_status = RUNNING
loop 执行每个步骤
alt 调用子设备
ROS->>ROS: execute_single_action(device_id, action, params)
ROS->>DEV: 发送Action Goal(通过Action Client)
DEV->>DEV: 执行设备动作
DEV-->>ROS: 返回Result
else 调用工作站自身
ROS->>WS: call_device_method(method, *args)
alt 直接模式
WS->>HW: 调用hardware_interface方法
HW->>HW: 执行硬件操作
HW-->>WS: 返回结果
else 代理模式
WS->>ROS: 转发到子设备
ROS->>DEV: 调用子设备方法
DEV-->>ROS: 返回结果
ROS-->>WS: 返回结果
end
WS-->>ROS: 返回结果
end
ROS->>DECK: 更新本地物料状态
DECK->>DECK: 修改PLR资源属性
end
ROS->>CLOUD: 同步物料到云端(可选)
CLOUD-->>ROS: 同步完成
ROS->>WS: 更新workflow_status = COMPLETED
ROS-->>CLIENT: 返回Protocol Result
```
## 5. HTTP报送处理时序图
```{mermaid}
sequenceDiagram
participant EXT as 外部工作站/LIMS
participant HTTP as HTTPService
participant WS as WorkstationBase
participant DECK as PLR Deck
participant SYNC as ResourceSynchronizer
participant CLOUD as 云端
EXT->>HTTP: POST /report/step_finish
HTTP->>HTTP: 解析请求数据
HTTP->>HTTP: 验证LIMS协议字段
HTTP->>WS: process_step_finish_report(request)
WS->>WS: 增加接收计数(_reports_received_count++)
WS->>WS: 记录步骤完成事件
WS->>DECK: 更新相关物料状态(可选)
DECK->>DECK: 修改PLR资源状态
WS->>WS: 保存报送记录到内存
WS-->>HTTP: 返回处理结果
HTTP->>HTTP: 构造HTTP响应
HTTP-->>EXT: 200 OK + acknowledgment_id
Note over EXT,CLOUD: 类似处理sample_finish, order_finish等报送
alt 物料变更报送
EXT->>HTTP: POST /report/material_change
HTTP->>WS: process_material_change_report(data)
WS->>DECK: 查找或创建物料
WS->>SYNC: sync_to_external(resource)
SYNC->>SYNC: 同步到外部系统(如Bioyond)
SYNC-->>WS: 同步完成
WS->>CLOUD: update_resource(通过ROS节点)
CLOUD-->>WS: 上传完成
WS-->>HTTP: 返回结果
HTTP-->>EXT: 200 OK
end
```
## 6. 错误处理时序图
```{mermaid}
sequenceDiagram
participant DEV as 子设备/外部系统
participant ROS as ROS2WorkstationNode
participant WS as WorkstationBase
participant HW as HardwareInterface
participant HTTP as HTTPService
participant LOG as 日志系统
alt 设备错误(ROS Action失败)
DEV->>ROS: Action返回失败结果
ROS->>ROS: 记录错误信息
ROS->>WS: 更新workflow_status = ERROR
ROS->>LOG: 记录错误日志
else 外部系统错误报送
DEV->>HTTP: POST /report/error_handling
HTTP->>WS: handle_external_error(error_data)
WS->>WS: 记录错误历史
WS->>LOG: 记录错误日志
end
alt 关键错误需要停止
WS->>ROS: stop_workflow(emergency=True)
ROS->>ROS: 取消所有进行中的Action
ROS->>HW: 调用emergency_stop()(如果支持)
HW->>HW: 执行紧急停止
WS->>WS: 更新workflow_status = ERROR
else 可恢复错误
WS->>WS: 标记步骤失败
WS->>ROS: 触发重试逻辑(可选)
ROS->>DEV: 重新发送Action
end
WS-->>HTTP: 返回错误处理结果
HTTP-->>DEV: 200 OK + 处理状态
```
## 7. 典型工作站实现示例
### 7.1 Bioyond集成工作站实现
```python
class BioyondWorkstation(WorkstationBase):
def __init__(self, bioyond_config: Dict, deck: Deck, *args, **kwargs):
# 初始化deck
super().__init__(deck=deck, *args, **kwargs)
# 设置硬件接口为Bioyond RPC客户端
self.hardware_interface = BioyondV1RPC(bioyond_config)
# 创建资源同步器
self.resource_synchronizer = BioyondResourceSynchronizer(self)
# 从Bioyond同步物料到本地deck
self.resource_synchronizer.sync_from_external()
# 配置工作流
self.workflow_mappings = bioyond_config.get("workflow_mappings", {})
def post_init(self, ros_node: ROS2WorkstationNode):
"""ROS节点就绪后的初始化"""
self._ros_node = ros_node
# 上传deck(包括所有物料)到云端
ROS2DeviceNode.run_async_func(
self._ros_node.update_resource,
True,
resources=[self.deck]
)
def resource_tree_add(self, resources: List[ResourcePLR]):
"""添加物料并同步到Bioyond"""
for resource in resources:
self.deck.assign_child_resource(resource, location)
self.resource_synchronizer.sync_to_external(resource)
```
### 7.2 纯协议节点实现
```python
class ProtocolNode(WorkstationBase):
"""纯协议节点,不需要物料管理和外部通信"""
def __init__(self, deck: Optional[Deck] = None, *args, **kwargs):
super().__init__(deck=deck, *args, **kwargs)
# 不设置hardware_interface和resource_synchronizer
# 所有功能通过子设备协同完成
def post_init(self, ros_node: ROS2WorkstationNode):
self._ros_node = ros_node
# 不需要上传物料或其他初始化
```
### 7.3 PLC直接控制工作站
```python
class PLCWorkstation(WorkstationBase):
def __init__(self, plc_config: Dict, deck: Deck, *args, **kwargs):
super().__init__(deck=deck, *args, **kwargs)
# 设置硬件接口为Modbus客户端
from pymodbus.client import ModbusTcpClient
self.hardware_interface = ModbusTcpClient(
host=plc_config["host"],
port=plc_config["port"]
)
self.hardware_interface.connect()
# 定义支持的工作流
self.supported_workflows = {
"battery_assembly": WorkflowInfo(
name="电池组装",
description="自动化电池组装流程",
estimated_duration=300.0,
required_materials=["battery_cell", "connector"],
output_product="battery_pack",
parameters_schema={"quantity": int, "model": str}
)
}
def execute_workflow(self, workflow_name: str, parameters: Dict):
"""通过PLC执行工作流"""
workflow_id = self._get_workflow_id(workflow_name)
# 写入PLC寄存器启动工作流
self.hardware_interface.write_register(100, workflow_id)
self.hardware_interface.write_register(101, parameters["quantity"])
self.current_workflow_status = WorkflowStatus.RUNNING
return True
```
## 8. 核心接口说明
### 8.1 WorkstationBase核心属性
| 属性 | 类型 | 说明 |
| --------------------------- | ----------------------- | ----------------------------- |
| `_ros_node` | ROS2WorkstationNode | ROS节点引用由post_init设置 |
| `deck` | Deck | PyLabRobot Deck本地物料系统 |
| `plr_resources` | Dict[str, PLRResource] | 物料资源映射 |
| `resource_synchronizer` | ResourceSynchronizer | 外部物料同步器(可选) |
| `hardware_interface` | Union[Any, str] | 硬件接口或代理字符串 |
| `current_workflow_status` | WorkflowStatus | 当前工作流状态 |
| `supported_workflows` | Dict[str, WorkflowInfo] | 支持的工作流定义 |
### 8.2 必须实现的方法
- `post_init(ros_node)`: ROS节点就绪后的初始化必须实现
### 8.3 硬件接口相关方法
- `set_hardware_interface(interface)`: 设置硬件接口
- `call_device_method(method, *args, **kwargs)`: 统一设备方法调用
- 支持直接模式: 直接调用hardware_interface的方法
- 支持代理模式: hardware_interface="proxy:device_id"通过ROS转发
- `get_device_status()`: 获取设备状态
- `is_device_available()`: 检查设备可用性
### 8.4 物料管理方法
- `get_deck()`: 获取PLR Deck
- `get_all_resources()`: 获取所有物料
- `find_resource_by_name(name)`: 按名称查找物料
- `find_resources_by_type(type)`: 按类型查找物料
- `sync_with_external_system()`: 触发外部同步
### 8.5 工作流控制方法
- `execute_workflow(name, params)`: 执行工作流
- `stop_workflow(emergency)`: 停止工作流
- `workflow_status`: 获取工作流状态(属性)
- `is_busy`: 检查是否忙碌(属性)
- `workflow_runtime`: 获取运行时间(属性)
### 8.6 可选的HTTP报送处理方法
- `process_step_finish_report()`: 步骤完成处理
- `process_sample_finish_report()`: 样本完成处理
- `process_order_finish_report()`: 订单完成处理
- `process_material_change_report()`: 物料变更处理
- `handle_external_error()`: 错误处理
### 8.7 ROS2WorkstationNode核心方法
- `initialize_device(device_id, config)`: 初始化子设备
- `create_ros_action_server(action_name, mapping)`: 创建Action服务器
- `execute_single_action(device_id, action, kwargs)`: 执行单个动作
- `update_resource(resources)`: 同步物料到云端
- `transfer_resource_to_another(...)`: 跨设备物料转移
## 9. 配置参数说明
### 9.1 工作站初始化配置
```python
# 示例1: Bioyond集成工作站
bioyond_config = {
"base_url": "http://192.168.1.100:8080",
"api_key": "your_api_key",
"sync_interval": 600, # 同步间隔(秒)
"workflow_mappings": {
"样品制备": "workflow_uuid_1",
"质检流程": "workflow_uuid_2"
},
"material_type_mappings": {
"plate": "板",
"tube": "试管"
},
"warehouse_mapping": {
"冷藏区": {
"uuid": "warehouse_uuid_1",
"locations": {...}
}
}
}
# 创建Deck
from pylabrobot.resources import Deck
deck = Deck(name="main_deck", size_x=1000, size_y=800, size_z=200)
workstation = BioyondWorkstation(
bioyond_config=bioyond_config,
deck=deck
)
```
### 9.2 子设备配置(children)
```python
# 在devices.json中配置
{
"bioyond_workstation": {
"type": "protocol", # 表示这是工作站节点
"protocol_type": ["LiquidHandling", "PlateHandling"],
"children": {
"pump_1": {
"type": "device",
"driver": "TricontInnovaDriver",
"communication": "serial_1",
"config": {...}
},
"gripper_1": {
"type": "device",
"driver": "RobotiqGripperDriver",
"communication": "io_modbus_1",
"config": {...}
},
"serial_1": {
"type": "communication",
"protocol": "serial",
"port": "/dev/ttyUSB0",
"baudrate": 9600
},
"io_modbus_1": {
"type": "communication",
"protocol": "modbus_tcp",
"host": "192.168.1.101",
"port": 502
}
}
}
}
```
### 9.3 HTTP服务配置
```python
from unilabos.devices.workstation.workstation_http_service import WorkstationHTTPService
# 创建HTTP服务(可选)
http_service = WorkstationHTTPService(
workstation_instance=workstation,
host="0.0.0.0", # 监听所有网卡
port=8081
)
http_service.start()
```
## 10. 架构设计特点总结
这个简化后的架构设计具有以下特点:
### 10.1 清晰的职责分离
- **WorkstationBase**: 负责物料管理(deck)、硬件接口(hardware_interface)、工作流状态管理
- **ROS2WorkstationNode**: 负责子设备管理、Protocol执行、云端物料同步
- **ResourceSynchronizer**: 可选的外部物料系统同步(如Bioyond)
- **WorkstationHTTPService**: 可选的HTTP报送接收服务
### 10.2 灵活的硬件接口模式
1. **直接模式**: hardware_interface是具体对象(如BioyondV1RPC、ModbusClient)
2. **代理模式**: hardware_interface="proxy:device_id"通过ROS节点转发到子设备
3. **混合模式**: 工作站有自己的接口,同时管理多个子设备
### 10.3 统一的物料系统
- 基于PyLabRobot Deck的标准化物料表示
- 通过ResourceSynchronizer实现与外部系统(如Bioyond、LIMS)的双向同步
- 通过ROS2WorkstationNode实现与云端的物料状态同步
### 10.4 Protocol驱动的工作流
- ROS2WorkstationNode负责Protocol的执行和步骤管理
- 支持子设备协同(通过Action Client调用)
- 支持工作站直接控制(通过hardware_interface)
### 10.5 可选的HTTP报送服务
- 基于LIMS协议规范的统一报送接口
- 支持步骤完成、样本完成、任务完成、物料变更等多种报送类型
- 与工作站解耦,可独立启停
### 10.6 简化的初始化流程
```
1. __init__: 创建deck、设置hardware_interface、创建resource_synchronizer
2. 从外部系统同步物料(如果有)
3. ROS节点初始化子设备
4. post_init: 关联ROS节点、上传物料到云端
5. (可选)启动HTTP服务
```
这种设计既保持了灵活性,又避免了过度抽象,更适合实际的工作站对接场景。