mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-17 04:51:10 +00:00
Merge branch 'workstation_dev' into dev
# Conflicts: # .conda/recipe.yaml # recipes/msgs/recipe.yaml # recipes/unilabos/recipe.yaml # setup.py # unilabos/registry/devices/work_station.yaml # unilabos/ros/nodes/base_device_node.py # unilabos/ros/nodes/presets/protocol_node.py # unilabos_msgs/package.xml
This commit is contained in:
@@ -19,7 +19,7 @@ Uni-Lab 的组态图当前支持 node-link json 和 graphml 格式,其中包
|
|||||||
对用户来说,“直接操作设备执行单个指令”不是个真实需求,真正的需求是**“执行对实验有意义的单个完整动作”——加入某种液体多少量;萃取分液;洗涤仪器等等。就像实验步骤文字书写的那样。**
|
对用户来说,“直接操作设备执行单个指令”不是个真实需求,真正的需求是**“执行对实验有意义的单个完整动作”——加入某种液体多少量;萃取分液;洗涤仪器等等。就像实验步骤文字书写的那样。**
|
||||||
|
|
||||||
而这些对实验有意义的单个完整动作,**一般需要多个设备的协同**,还依赖于他们的**物理连接关系(管道相连;机械臂可转运)**。
|
而这些对实验有意义的单个完整动作,**一般需要多个设备的协同**,还依赖于他们的**物理连接关系(管道相连;机械臂可转运)**。
|
||||||
于是 Uni-Lab 实现了抽象的“工作站”,即注册表中的 `workstation` 设备(`ProtocolNode`类)来处理编译、规划操作。以泵骨架组成的自动有机实验室为例,设备管道连接关系如下:
|
于是 Uni-Lab 实现了抽象的“工作站”,即注册表中的 `workstation` 设备(`WorkstationNode`类)来处理编译、规划操作。以泵骨架组成的自动有机实验室为例,设备管道连接关系如下:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
378
docs/developer_guide/workstation_architecture.md
Normal file
378
docs/developer_guide/workstation_architecture.md
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
# 工作站基础架构设计文档
|
||||||
|
|
||||||
|
## 1. 整体架构图
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph "工作站基础架构"
|
||||||
|
WB[WorkstationBase]
|
||||||
|
WB --> |继承| RPN[ROS2WorkstationNode]
|
||||||
|
WB --> |组合| WCB[WorkstationCommunicationBase]
|
||||||
|
WB --> |组合| MMB[MaterialManagementBase]
|
||||||
|
WB --> |组合| WHS[WorkstationHTTPService]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "通信层实现"
|
||||||
|
WCB --> |实现| PLC[PLCCommunication]
|
||||||
|
WCB --> |实现| SER[SerialCommunication]
|
||||||
|
WCB --> |实现| ETH[EthernetCommunication]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "物料管理实现"
|
||||||
|
MMB --> |实现| PLR[PyLabRobotMaterialManager]
|
||||||
|
MMB --> |实现| BIO[BioyondMaterialManager]
|
||||||
|
MMB --> |实现| SIM[SimpleMaterialManager]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "HTTP服务"
|
||||||
|
WHS --> |处理| LIMS[LIMS协议报送]
|
||||||
|
WHS --> |处理| MAT[物料变更报送]
|
||||||
|
WHS --> |处理| ERR[错误处理报送]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "具体工作站实现"
|
||||||
|
WB --> |继承| WS1[PLCWorkstation]
|
||||||
|
WB --> |继承| WS2[ReportingWorkstation]
|
||||||
|
WB --> |继承| WS3[HybridWorkstation]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "外部系统"
|
||||||
|
EXT1[PLC设备] --> |通信| PLC
|
||||||
|
EXT2[外部工作站] --> |HTTP报送| WHS
|
||||||
|
EXT3[LIMS系统] --> |HTTP报送| WHS
|
||||||
|
EXT4[Bioyond物料系统] --> |查询| BIO
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 类关系图
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class WorkstationBase {
|
||||||
|
<<abstract>>
|
||||||
|
+device_id: str
|
||||||
|
+communication: WorkstationCommunicationBase
|
||||||
|
+material_management: MaterialManagementBase
|
||||||
|
+http_service: WorkstationHTTPService
|
||||||
|
+workflow_status: WorkflowStatus
|
||||||
|
+supported_workflows: Dict
|
||||||
|
|
||||||
|
+_create_communication_module()*
|
||||||
|
+_create_material_management_module()*
|
||||||
|
+_register_supported_workflows()*
|
||||||
|
|
||||||
|
+process_step_finish_report()
|
||||||
|
+process_sample_finish_report()
|
||||||
|
+process_order_finish_report()
|
||||||
|
+process_material_change_report()
|
||||||
|
+handle_external_error()
|
||||||
|
|
||||||
|
+start_workflow()
|
||||||
|
+stop_workflow()
|
||||||
|
+get_workflow_status()
|
||||||
|
+get_device_status()
|
||||||
|
}
|
||||||
|
|
||||||
|
class ROS2WorkstationNode {
|
||||||
|
+sub_devices: Dict
|
||||||
|
+protocol_names: List
|
||||||
|
+execute_single_action()
|
||||||
|
+create_ros_action_server()
|
||||||
|
+initialize_device()
|
||||||
|
}
|
||||||
|
|
||||||
|
class WorkstationCommunicationBase {
|
||||||
|
<<abstract>>
|
||||||
|
+config: CommunicationConfig
|
||||||
|
+is_connected: bool
|
||||||
|
+connect()
|
||||||
|
+disconnect()
|
||||||
|
+start_workflow()*
|
||||||
|
+stop_workflow()*
|
||||||
|
+get_device_status()*
|
||||||
|
+write_register()
|
||||||
|
+read_register()
|
||||||
|
}
|
||||||
|
|
||||||
|
class MaterialManagementBase {
|
||||||
|
<<abstract>>
|
||||||
|
+device_id: str
|
||||||
|
+deck_config: Dict
|
||||||
|
+resource_tracker: DeviceNodeResourceTracker
|
||||||
|
+plr_deck: Deck
|
||||||
|
+find_materials_by_type()
|
||||||
|
+update_material_location()
|
||||||
|
+convert_to_unilab_format()
|
||||||
|
+_create_resource_by_type()*
|
||||||
|
}
|
||||||
|
|
||||||
|
class WorkstationHTTPService {
|
||||||
|
+workstation_instance: WorkstationBase
|
||||||
|
+host: str
|
||||||
|
+port: int
|
||||||
|
+start()
|
||||||
|
+stop()
|
||||||
|
+_handle_step_finish_report()
|
||||||
|
+_handle_material_change_report()
|
||||||
|
}
|
||||||
|
|
||||||
|
class PLCWorkstation {
|
||||||
|
+plc_config: Dict
|
||||||
|
+modbus_client: ModbusTCPClient
|
||||||
|
+_create_communication_module()
|
||||||
|
+_create_material_management_module()
|
||||||
|
+_register_supported_workflows()
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReportingWorkstation {
|
||||||
|
+report_handlers: Dict
|
||||||
|
+_create_communication_module()
|
||||||
|
+_create_material_management_module()
|
||||||
|
+_register_supported_workflows()
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkstationBase --|> ROS2WorkstationNode
|
||||||
|
WorkstationBase *-- WorkstationCommunicationBase
|
||||||
|
WorkstationBase *-- MaterialManagementBase
|
||||||
|
WorkstationBase *-- WorkstationHTTPService
|
||||||
|
|
||||||
|
PLCWorkstation --|> WorkstationBase
|
||||||
|
ReportingWorkstation --|> WorkstationBase
|
||||||
|
|
||||||
|
WorkstationCommunicationBase <|-- PLCCommunication
|
||||||
|
WorkstationCommunicationBase <|-- DummyCommunication
|
||||||
|
|
||||||
|
MaterialManagementBase <|-- PyLabRobotMaterialManager
|
||||||
|
MaterialManagementBase <|-- SimpleMaterialManager
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 工作站启动时序图
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant APP as Application
|
||||||
|
participant WS as WorkstationBase
|
||||||
|
participant COMM as CommunicationModule
|
||||||
|
participant MAT as MaterialManager
|
||||||
|
participant HTTP as HTTPService
|
||||||
|
participant ROS as ROS2WorkstationNode
|
||||||
|
|
||||||
|
APP->>WS: 创建工作站实例
|
||||||
|
WS->>ROS: 初始化ROS2WorkstationNode
|
||||||
|
ROS->>ROS: 初始化子设备
|
||||||
|
ROS->>ROS: 设置硬件接口代理
|
||||||
|
|
||||||
|
WS->>COMM: _create_communication_module()
|
||||||
|
COMM->>COMM: 初始化通信配置
|
||||||
|
COMM->>COMM: 建立PLC/串口连接
|
||||||
|
COMM-->>WS: 返回通信模块实例
|
||||||
|
|
||||||
|
WS->>MAT: _create_material_management_module()
|
||||||
|
MAT->>MAT: 创建PyLabRobot Deck
|
||||||
|
MAT->>MAT: 初始化物料资源
|
||||||
|
MAT->>MAT: 注册到ResourceTracker
|
||||||
|
MAT-->>WS: 返回物料管理实例
|
||||||
|
|
||||||
|
WS->>WS: _register_supported_workflows()
|
||||||
|
WS->>WS: _create_workstation_services()
|
||||||
|
WS->>HTTP: _start_http_service()
|
||||||
|
HTTP->>HTTP: 创建HTTP服务器
|
||||||
|
HTTP->>HTTP: 启动监听线程
|
||||||
|
HTTP-->>WS: HTTP服务启动完成
|
||||||
|
|
||||||
|
WS-->>APP: 工作站初始化完成
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 工作流执行时序图
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant EXT as ExternalSystem
|
||||||
|
participant WS as WorkstationBase
|
||||||
|
participant COMM as CommunicationModule
|
||||||
|
participant MAT as MaterialManager
|
||||||
|
participant ROS as ROS2WorkstationNode
|
||||||
|
participant DEV as SubDevice
|
||||||
|
|
||||||
|
EXT->>WS: start_workflow(type, params)
|
||||||
|
WS->>WS: 验证工作流类型
|
||||||
|
WS->>COMM: start_workflow(type, params)
|
||||||
|
COMM->>COMM: 发送启动命令到PLC
|
||||||
|
COMM-->>WS: 启动成功
|
||||||
|
|
||||||
|
WS->>WS: 更新workflow_status = RUNNING
|
||||||
|
|
||||||
|
loop 工作流步骤执行
|
||||||
|
WS->>ROS: execute_single_action(device_id, action, params)
|
||||||
|
ROS->>DEV: 发送ROS Action请求
|
||||||
|
DEV->>DEV: 执行设备动作
|
||||||
|
DEV-->>ROS: 返回执行结果
|
||||||
|
ROS-->>WS: 返回动作结果
|
||||||
|
|
||||||
|
WS->>MAT: update_material_location(material_id, location)
|
||||||
|
MAT->>MAT: 更新PyLabRobot资源状态
|
||||||
|
MAT-->>WS: 更新完成
|
||||||
|
end
|
||||||
|
|
||||||
|
WS->>COMM: get_workflow_status()
|
||||||
|
COMM->>COMM: 查询PLC状态寄存器
|
||||||
|
COMM-->>WS: 返回状态信息
|
||||||
|
|
||||||
|
WS->>WS: 更新workflow_status = COMPLETED
|
||||||
|
WS-->>EXT: 工作流执行完成
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. HTTP报送处理时序图
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant EXT as ExternalWorkstation
|
||||||
|
participant HTTP as HTTPService
|
||||||
|
participant WS as WorkstationBase
|
||||||
|
participant MAT as MaterialManager
|
||||||
|
participant DB as DataStorage
|
||||||
|
|
||||||
|
EXT->>HTTP: POST /report/step_finish
|
||||||
|
HTTP->>HTTP: 解析请求数据
|
||||||
|
HTTP->>HTTP: 验证LIMS协议字段
|
||||||
|
HTTP->>WS: process_step_finish_report(request)
|
||||||
|
|
||||||
|
WS->>WS: 增加接收计数
|
||||||
|
WS->>WS: 记录步骤完成事件
|
||||||
|
WS->>MAT: 更新相关物料状态
|
||||||
|
MAT->>MAT: 更新PyLabRobot资源
|
||||||
|
MAT-->>WS: 更新完成
|
||||||
|
|
||||||
|
WS->>DB: 保存报送记录
|
||||||
|
DB-->>WS: 保存完成
|
||||||
|
|
||||||
|
WS-->>HTTP: 返回处理结果
|
||||||
|
HTTP->>HTTP: 构造HTTP响应
|
||||||
|
HTTP-->>EXT: 200 OK + acknowledgment_id
|
||||||
|
|
||||||
|
Note over EXT,DB: 类似处理sample_finish, order_finish, material_change等报送
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 错误处理时序图
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant DEV as Device
|
||||||
|
participant WS as WorkstationBase
|
||||||
|
participant COMM as CommunicationModule
|
||||||
|
participant HTTP as HTTPService
|
||||||
|
participant EXT as ExternalSystem
|
||||||
|
|
||||||
|
DEV->>WS: 设备错误事件
|
||||||
|
WS->>WS: handle_external_error(error_data)
|
||||||
|
WS->>WS: 记录错误历史
|
||||||
|
|
||||||
|
alt 关键错误
|
||||||
|
WS->>COMM: emergency_stop()
|
||||||
|
COMM->>COMM: 发送紧急停止命令
|
||||||
|
WS->>WS: 更新workflow_status = ERROR
|
||||||
|
else 普通错误
|
||||||
|
WS->>WS: 标记动作失败
|
||||||
|
WS->>WS: 触发重试逻辑
|
||||||
|
end
|
||||||
|
|
||||||
|
WS->>HTTP: 记录错误报送
|
||||||
|
HTTP->>EXT: 主动通知错误状态
|
||||||
|
|
||||||
|
WS-->>DEV: 错误处理完成
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 典型工作站实现示例
|
||||||
|
|
||||||
|
### 7.1 PLC工作站实现
|
||||||
|
|
||||||
|
```python
|
||||||
|
class PLCWorkstation(WorkstationBase):
|
||||||
|
def _create_communication_module(self):
|
||||||
|
return PLCCommunication(self.communication_config)
|
||||||
|
|
||||||
|
def _create_material_management_module(self):
|
||||||
|
return PyLabRobotMaterialManager(
|
||||||
|
self.device_id,
|
||||||
|
self.deck_config,
|
||||||
|
self.resource_tracker
|
||||||
|
)
|
||||||
|
|
||||||
|
def _register_supported_workflows(self):
|
||||||
|
self.supported_workflows = {
|
||||||
|
"battery_assembly": WorkflowInfo(...),
|
||||||
|
"quality_check": WorkflowInfo(...)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 报送接收工作站实现
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ReportingWorkstation(WorkstationBase):
|
||||||
|
def _create_communication_module(self):
|
||||||
|
return DummyCommunication(self.communication_config)
|
||||||
|
|
||||||
|
def _create_material_management_module(self):
|
||||||
|
return SimpleMaterialManager(
|
||||||
|
self.device_id,
|
||||||
|
self.deck_config,
|
||||||
|
self.resource_tracker
|
||||||
|
)
|
||||||
|
|
||||||
|
def _register_supported_workflows(self):
|
||||||
|
self.supported_workflows = {
|
||||||
|
"data_collection": WorkflowInfo(...),
|
||||||
|
"report_processing": WorkflowInfo(...)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 核心接口说明
|
||||||
|
|
||||||
|
### 8.1 必须实现的抽象方法
|
||||||
|
- `_create_communication_module()`: 创建通信模块
|
||||||
|
- `_create_material_management_module()`: 创建物料管理模块
|
||||||
|
- `_register_supported_workflows()`: 注册支持的工作流
|
||||||
|
|
||||||
|
### 8.2 可重写的报送处理方法
|
||||||
|
- `process_step_finish_report()`: 步骤完成处理
|
||||||
|
- `process_sample_finish_report()`: 样本完成处理
|
||||||
|
- `process_order_finish_report()`: 订单完成处理
|
||||||
|
- `process_material_change_report()`: 物料变更处理
|
||||||
|
- `handle_external_error()`: 错误处理
|
||||||
|
|
||||||
|
### 8.3 工作流控制接口
|
||||||
|
- `start_workflow()`: 启动工作流
|
||||||
|
- `stop_workflow()`: 停止工作流
|
||||||
|
- `get_workflow_status()`: 获取状态
|
||||||
|
|
||||||
|
## 9. 配置参数说明
|
||||||
|
|
||||||
|
```python
|
||||||
|
workstation_config = {
|
||||||
|
"communication_config": {
|
||||||
|
"protocol": "modbus_tcp",
|
||||||
|
"host": "192.168.1.100",
|
||||||
|
"port": 502
|
||||||
|
},
|
||||||
|
"deck_config": {
|
||||||
|
"size_x": 1000.0,
|
||||||
|
"size_y": 1000.0,
|
||||||
|
"size_z": 500.0
|
||||||
|
},
|
||||||
|
"http_service_config": {
|
||||||
|
"enabled": True,
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 8081
|
||||||
|
},
|
||||||
|
"communication_interfaces": {
|
||||||
|
"logical_device_1": CommunicationInterface(...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
这个架构设计支持:
|
||||||
|
1. **灵活的通信方式**: 通过CommunicationBase支持PLC、串口、以太网等
|
||||||
|
2. **多样的物料管理**: 支持PyLabRobot、Bioyond、简单物料系统
|
||||||
|
3. **统一的HTTP报送**: 基于LIMS协议的标准化报送接口
|
||||||
|
4. **完整的工作流控制**: 支持动态和静态工作流
|
||||||
|
5. **强大的错误处理**: 多层次的错误处理和恢复机制
|
||||||
File diff suppressed because it is too large
Load Diff
454
unilabos/device_comms/coin_cell_assembly_workstation.py
Normal file
454
unilabos/device_comms/coin_cell_assembly_workstation.py
Normal file
@@ -0,0 +1,454 @@
|
|||||||
|
"""
|
||||||
|
纽扣电池组装工作站
|
||||||
|
Coin Cell Assembly Workstation
|
||||||
|
|
||||||
|
继承工作站基类,实现纽扣电池特定功能
|
||||||
|
"""
|
||||||
|
from typing import Dict, Any, List, Optional, Union
|
||||||
|
|
||||||
|
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
|
||||||
|
from unilabos.device_comms.workstation_base import WorkstationBase, WorkflowInfo
|
||||||
|
from unilabos.device_comms.workstation_communication import (
|
||||||
|
WorkstationCommunicationBase, CommunicationConfig, CommunicationProtocol, CoinCellCommunication
|
||||||
|
)
|
||||||
|
from unilabos.device_comms.workstation_material_management import (
|
||||||
|
MaterialManagementBase, CoinCellMaterialManagement
|
||||||
|
)
|
||||||
|
from unilabos.utils.log import logger
|
||||||
|
|
||||||
|
|
||||||
|
class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||||
|
"""纽扣电池组装工作站
|
||||||
|
|
||||||
|
基于工作站基类,实现纽扣电池制造的特定功能:
|
||||||
|
1. 纽扣电池特定的通信协议
|
||||||
|
2. 纽扣电池物料管理(料板、极片、电池等)
|
||||||
|
3. 电池制造工作流
|
||||||
|
4. 质量检查工作流
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
device_id: str,
|
||||||
|
children: Dict[str, Dict[str, Any]],
|
||||||
|
protocol_type: Union[str, List[str]] = "BatteryManufacturingProtocol",
|
||||||
|
resource_tracker: Optional[DeviceNodeResourceTracker] = None,
|
||||||
|
modbus_config: Optional[Dict[str, Any]] = None,
|
||||||
|
deck_config: Optional[Dict[str, Any]] = None,
|
||||||
|
csv_path: str = "./coin_cell_assembly.csv",
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
# 设置通信配置
|
||||||
|
modbus_config = modbus_config or {"host": "127.0.0.1", "port": 5021}
|
||||||
|
self.communication_config = CommunicationConfig(
|
||||||
|
protocol=CommunicationProtocol.MODBUS_TCP,
|
||||||
|
host=modbus_config["host"],
|
||||||
|
port=modbus_config["port"],
|
||||||
|
timeout=modbus_config.get("timeout", 5.0),
|
||||||
|
retry_count=modbus_config.get("retry_count", 3)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 设置台面配置
|
||||||
|
self.deck_config = deck_config or {
|
||||||
|
"size_x": 1620.0,
|
||||||
|
"size_y": 1270.0,
|
||||||
|
"size_z": 500.0
|
||||||
|
}
|
||||||
|
|
||||||
|
# CSV地址映射文件路径
|
||||||
|
self.csv_path = csv_path
|
||||||
|
|
||||||
|
# 创建资源跟踪器(如果没有提供)
|
||||||
|
if resource_tracker is None:
|
||||||
|
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
|
||||||
|
resource_tracker = DeviceNodeResourceTracker()
|
||||||
|
|
||||||
|
# 初始化基类
|
||||||
|
super().__init__(
|
||||||
|
device_id=device_id,
|
||||||
|
children=children,
|
||||||
|
protocol_type=protocol_type,
|
||||||
|
resource_tracker=resource_tracker,
|
||||||
|
communication_config=self.communication_config,
|
||||||
|
deck_config=self.deck_config,
|
||||||
|
*args,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"纽扣电池组装工作站 {device_id} 初始化完成")
|
||||||
|
|
||||||
|
def _create_communication_module(self) -> WorkstationCommunicationBase:
|
||||||
|
"""创建纽扣电池通信模块"""
|
||||||
|
return CoinCellCommunication(
|
||||||
|
communication_config=self.communication_config,
|
||||||
|
csv_path=self.csv_path
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_material_management_module(self) -> MaterialManagementBase:
|
||||||
|
"""创建纽扣电池物料管理模块"""
|
||||||
|
return CoinCellMaterialManagement(
|
||||||
|
device_id=self.device_id,
|
||||||
|
deck_config=self.deck_config,
|
||||||
|
resource_tracker=self.resource_tracker,
|
||||||
|
children_config=self.children
|
||||||
|
)
|
||||||
|
|
||||||
|
def _register_supported_workflows(self):
|
||||||
|
"""注册纽扣电池工作流"""
|
||||||
|
# 电池制造工作流
|
||||||
|
self.supported_workflows["battery_manufacturing"] = WorkflowInfo(
|
||||||
|
name="battery_manufacturing",
|
||||||
|
description="纽扣电池制造工作流",
|
||||||
|
estimated_duration=300.0, # 5分钟
|
||||||
|
required_materials=["cathode_sheet", "anode_sheet", "separator", "electrolyte"],
|
||||||
|
output_product="coin_cell_battery",
|
||||||
|
parameters_schema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"electrolyte_num": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "电解液瓶数",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 32
|
||||||
|
},
|
||||||
|
"electrolyte_volume": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "电解液体积 (μL)",
|
||||||
|
"minimum": 0.1,
|
||||||
|
"maximum": 100.0
|
||||||
|
},
|
||||||
|
"assembly_pressure": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "组装压力 (N)",
|
||||||
|
"minimum": 100.0,
|
||||||
|
"maximum": 5000.0
|
||||||
|
},
|
||||||
|
"cathode_material": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "正极材料类型",
|
||||||
|
"enum": ["LiFePO4", "LiCoO2", "NCM", "LMO"]
|
||||||
|
},
|
||||||
|
"anode_material": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "负极材料类型",
|
||||||
|
"enum": ["Graphite", "LTO", "Silicon"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["electrolyte_num", "electrolyte_volume", "assembly_pressure"]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 质量检查工作流
|
||||||
|
self.supported_workflows["quality_inspection"] = WorkflowInfo(
|
||||||
|
name="quality_inspection",
|
||||||
|
description="产品质量检查工作流",
|
||||||
|
estimated_duration=60.0, # 1分钟
|
||||||
|
required_materials=["finished_battery"],
|
||||||
|
output_product="quality_report",
|
||||||
|
parameters_schema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"test_voltage": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "是否测试电压",
|
||||||
|
"default": True
|
||||||
|
},
|
||||||
|
"test_capacity": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "是否测试容量",
|
||||||
|
"default": False
|
||||||
|
},
|
||||||
|
"voltage_threshold": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "电压阈值 (V)",
|
||||||
|
"minimum": 2.0,
|
||||||
|
"maximum": 4.5,
|
||||||
|
"default": 3.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 设备初始化工作流
|
||||||
|
self.supported_workflows["device_initialization"] = WorkflowInfo(
|
||||||
|
name="device_initialization",
|
||||||
|
description="设备初始化工作流",
|
||||||
|
estimated_duration=30.0, # 30秒
|
||||||
|
required_materials=[],
|
||||||
|
output_product="ready_status",
|
||||||
|
parameters_schema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"auto_mode": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "是否启用自动模式",
|
||||||
|
"default": True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# ============ 纽扣电池特定方法 ============
|
||||||
|
|
||||||
|
def get_electrode_sheet_inventory(self) -> Dict[str, int]:
|
||||||
|
"""获取极片库存统计"""
|
||||||
|
try:
|
||||||
|
sheets = self.material_management.find_electrode_sheets()
|
||||||
|
inventory = {}
|
||||||
|
|
||||||
|
for sheet in sheets:
|
||||||
|
material_type = getattr(sheet, 'material_type', 'unknown')
|
||||||
|
inventory[material_type] = inventory.get(material_type, 0) + 1
|
||||||
|
|
||||||
|
return inventory
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取极片库存失败: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def get_battery_production_statistics(self) -> Dict[str, Any]:
|
||||||
|
"""获取电池生产统计"""
|
||||||
|
try:
|
||||||
|
production_data = self.communication.get_production_data()
|
||||||
|
|
||||||
|
# 添加物料统计
|
||||||
|
electrode_inventory = self.get_electrode_sheet_inventory()
|
||||||
|
battery_count = len(self.material_management.find_batteries())
|
||||||
|
|
||||||
|
return {
|
||||||
|
**production_data,
|
||||||
|
"electrode_inventory": electrode_inventory,
|
||||||
|
"finished_battery_count": battery_count,
|
||||||
|
"material_plates": len(self.material_management.find_material_plates()),
|
||||||
|
"press_slots": len(self.material_management.find_press_slots())
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取生产统计失败: {e}")
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
def create_new_battery(self, battery_spec: Dict[str, Any]) -> Optional[str]:
|
||||||
|
"""创建新电池资源"""
|
||||||
|
try:
|
||||||
|
from unilabos.device_comms.button_battery_station import Battery
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
battery_id = f"battery_{uuid.uuid4().hex[:8]}"
|
||||||
|
|
||||||
|
battery = Battery(
|
||||||
|
name=battery_id,
|
||||||
|
diameter=battery_spec.get("diameter", 20.0),
|
||||||
|
height=battery_spec.get("height", 3.2),
|
||||||
|
max_volume=battery_spec.get("max_volume", 100.0),
|
||||||
|
barcode=battery_spec.get("barcode", "")
|
||||||
|
)
|
||||||
|
|
||||||
|
# 添加到物料管理系统
|
||||||
|
self.material_management.plr_resources[battery_id] = battery
|
||||||
|
self.material_management.resource_tracker.add_resource(battery)
|
||||||
|
|
||||||
|
logger.info(f"创建新电池资源: {battery_id}")
|
||||||
|
return battery_id
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"创建电池资源失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def find_available_press_slot(self) -> Optional[str]:
|
||||||
|
"""查找可用的压制槽"""
|
||||||
|
try:
|
||||||
|
press_slots = self.material_management.find_press_slots()
|
||||||
|
|
||||||
|
for slot in press_slots:
|
||||||
|
if hasattr(slot, 'has_battery') and not slot.has_battery():
|
||||||
|
return slot.name
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"查找可用压制槽失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_glove_box_environment(self) -> Dict[str, Any]:
|
||||||
|
"""获取手套箱环境数据"""
|
||||||
|
try:
|
||||||
|
device_status = self.communication.get_device_status()
|
||||||
|
environment = device_status.get("environment", {})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"pressure": environment.get("glove_box_pressure", 0.0),
|
||||||
|
"o2_content": environment.get("o2_content", 0.0),
|
||||||
|
"water_content": environment.get("water_content", 0.0),
|
||||||
|
"is_safe": (
|
||||||
|
environment.get("o2_content", 0.0) < 10.0 and # 氧气含量 < 10ppm
|
||||||
|
environment.get("water_content", 0.0) < 1.0 # 水分含量 < 1ppm
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取手套箱环境失败: {e}")
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
def start_data_export(self, file_path: str) -> bool:
|
||||||
|
"""开始生产数据导出"""
|
||||||
|
try:
|
||||||
|
return self.communication.start_data_export(file_path, export_interval=5.0)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"启动数据导出失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def stop_data_export(self) -> bool:
|
||||||
|
"""停止生产数据导出"""
|
||||||
|
try:
|
||||||
|
return self.communication.stop_data_export()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"停止数据导出失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ============ 重写基类方法以支持纽扣电池特定功能 ============
|
||||||
|
|
||||||
|
def start_workflow(self, workflow_type: str, parameters: Dict[str, Any] = None) -> bool:
|
||||||
|
"""启动工作流(重写以支持纽扣电池特定预处理)"""
|
||||||
|
try:
|
||||||
|
# 进行纽扣电池特定的预检查
|
||||||
|
if workflow_type == "battery_manufacturing":
|
||||||
|
# 检查手套箱环境
|
||||||
|
env = self.get_glove_box_environment()
|
||||||
|
if not env.get("is_safe", False):
|
||||||
|
logger.error("手套箱环境不安全,无法启动电池制造工作流")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 检查是否有可用的压制槽
|
||||||
|
available_slot = self.find_available_press_slot()
|
||||||
|
if not available_slot:
|
||||||
|
logger.error("没有可用的压制槽,无法启动电池制造工作流")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 检查极片库存
|
||||||
|
electrode_inventory = self.get_electrode_sheet_inventory()
|
||||||
|
if not electrode_inventory.get("cathode", 0) > 0 or not electrode_inventory.get("anode", 0) > 0:
|
||||||
|
logger.error("极片库存不足,无法启动电池制造工作流")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 调用基类方法
|
||||||
|
return super().start_workflow(workflow_type, parameters)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"启动纽扣电池工作流失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ============ 纽扣电池特定状态属性 ============
|
||||||
|
|
||||||
|
@property
|
||||||
|
def electrode_sheet_count(self) -> int:
|
||||||
|
"""极片总数"""
|
||||||
|
try:
|
||||||
|
return len(self.material_management.find_electrode_sheets())
|
||||||
|
except:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def battery_count(self) -> int:
|
||||||
|
"""电池总数"""
|
||||||
|
try:
|
||||||
|
return len(self.material_management.find_batteries())
|
||||||
|
except:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available_press_slots(self) -> int:
|
||||||
|
"""可用压制槽数"""
|
||||||
|
try:
|
||||||
|
press_slots = self.material_management.find_press_slots()
|
||||||
|
available = 0
|
||||||
|
for slot in press_slots:
|
||||||
|
if hasattr(slot, 'has_battery') and not slot.has_battery():
|
||||||
|
available += 1
|
||||||
|
return available
|
||||||
|
except:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def environment_status(self) -> Dict[str, Any]:
|
||||||
|
"""环境状态"""
|
||||||
|
return self.get_glove_box_environment()
|
||||||
|
|
||||||
|
|
||||||
|
# ============ 工厂函数 ============
|
||||||
|
|
||||||
|
def create_coin_cell_workstation(
|
||||||
|
device_id: str,
|
||||||
|
config_file: str,
|
||||||
|
modbus_host: str = "127.0.0.1",
|
||||||
|
modbus_port: int = 5021,
|
||||||
|
csv_path: str = "./coin_cell_assembly.csv"
|
||||||
|
) -> CoinCellAssemblyWorkstation:
|
||||||
|
"""工厂函数:创建纽扣电池组装工作站
|
||||||
|
|
||||||
|
Args:
|
||||||
|
device_id: 设备ID
|
||||||
|
config_file: 配置文件路径(JSON格式)
|
||||||
|
modbus_host: Modbus主机地址
|
||||||
|
modbus_port: Modbus端口
|
||||||
|
csv_path: 地址映射CSV文件路径
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
CoinCellAssemblyWorkstation: 工作站实例
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 加载配置文件
|
||||||
|
with open(config_file, 'r', encoding='utf-8') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
|
||||||
|
# 提取配置
|
||||||
|
children = config.get("children", {})
|
||||||
|
deck_config = config.get("deck_config", {})
|
||||||
|
|
||||||
|
# 创建工作站
|
||||||
|
workstation = CoinCellAssemblyWorkstation(
|
||||||
|
device_id=device_id,
|
||||||
|
children=children,
|
||||||
|
modbus_config={
|
||||||
|
"host": modbus_host,
|
||||||
|
"port": modbus_port
|
||||||
|
},
|
||||||
|
deck_config=deck_config,
|
||||||
|
csv_path=csv_path
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"纽扣电池工作站创建成功: {device_id}")
|
||||||
|
return workstation
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"创建纽扣电池工作站失败: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 示例用法
|
||||||
|
workstation = create_coin_cell_workstation(
|
||||||
|
device_id="coin_cell_station_01",
|
||||||
|
config_file="./button_battery_workstation.json",
|
||||||
|
modbus_host="127.0.0.1",
|
||||||
|
modbus_port=5021
|
||||||
|
)
|
||||||
|
|
||||||
|
# 启动电池制造工作流
|
||||||
|
success = workstation.start_workflow(
|
||||||
|
"battery_manufacturing",
|
||||||
|
{
|
||||||
|
"electrolyte_num": 16,
|
||||||
|
"electrolyte_volume": 50.0,
|
||||||
|
"assembly_pressure": 2000.0,
|
||||||
|
"cathode_material": "LiFePO4",
|
||||||
|
"anode_material": "Graphite"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print("电池制造工作流启动成功")
|
||||||
|
else:
|
||||||
|
print("电池制造工作流启动失败")
|
||||||
@@ -8,8 +8,8 @@ from pymodbus.client import ModbusSerialClient, ModbusTcpClient
|
|||||||
from pymodbus.framer import FramerType
|
from pymodbus.framer import FramerType
|
||||||
from typing import TypedDict
|
from typing import TypedDict
|
||||||
|
|
||||||
from unilabos.device_comms.modbus_plc.node.modbus import DeviceType, HoldRegister, Coil, InputRegister, DiscreteInputs, DataType, WorderOrder
|
from unilabos.device_comms.modbus_plc.modbus import DeviceType, HoldRegister, Coil, InputRegister, DiscreteInputs, DataType, WorderOrder
|
||||||
from unilabos.device_comms.modbus_plc.node.modbus import Base as ModbusNodeBase
|
from unilabos.device_comms.modbus_plc.modbus import Base as ModbusNodeBase
|
||||||
from unilabos.device_comms.universal_driver import UniversalDriver
|
from unilabos.device_comms.universal_driver import UniversalDriver
|
||||||
from unilabos.utils.log import logger
|
from unilabos.utils.log import logger
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import time
|
import time
|
||||||
from pymodbus.client import ModbusTcpClient
|
from pymodbus.client import ModbusTcpClient
|
||||||
from unilabos.device_comms.modbus_plc.node.modbus import Coil, HoldRegister
|
from unilabos.device_comms.modbus_plc.modbus import Coil, HoldRegister
|
||||||
from pymodbus.payload import BinaryPayloadDecoder
|
from pymodbus.payload import BinaryPayloadDecoder
|
||||||
from pymodbus.constants import Endian
|
from pymodbus.constants import Endian
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
from pymodbus.client import ModbusTcpClient
|
from pymodbus.client import ModbusTcpClient
|
||||||
from unilabos.device_comms.modbus_plc.node.modbus import Coil
|
from unilabos.device_comms.modbus_plc.modbus import Coil
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import time
|
import time
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from unilabos.device_comms.modbus_plc.client import TCPClient, ModbusWorkflow, WorkflowAction, load_csv
|
from unilabos.device_comms.modbus_plc.client import TCPClient, ModbusWorkflow, WorkflowAction, load_csv
|
||||||
from unilabos.device_comms.modbus_plc.node.modbus import Base as ModbusNodeBase
|
from unilabos.device_comms.modbus_plc.modbus import Base as ModbusNodeBase
|
||||||
|
|
||||||
############ 第一种写法 ##############
|
############ 第一种写法 ##############
|
||||||
|
|
||||||
|
|||||||
184
unilabos/devices/workstation/README.md
Normal file
184
unilabos/devices/workstation/README.md
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
# 工作站抽象基类物料系统架构说明
|
||||||
|
|
||||||
|
## 设计理念
|
||||||
|
|
||||||
|
基于用户需求"请你帮我系统思考一下,工作站抽象基类的物料系统基类该如何构建",我们最终确定了一个**PyLabRobot Deck为中心**的简化架构。
|
||||||
|
|
||||||
|
### 核心原则
|
||||||
|
|
||||||
|
1. **PyLabRobot为物料管理核心**:使用PyLabRobot的Deck系统作为物料管理的基础,利用其成熟的Resource体系
|
||||||
|
2. **Graphio转换函数集成**:使用graphio中的`resource_ulab_to_plr`等转换函数实现UniLab与PLR格式的无缝转换
|
||||||
|
3. **关注点分离**:基类专注核心物料系统,HTTP服务等功能在子类中实现
|
||||||
|
4. **外部系统集成模式**:通过ResourceSynchronizer抽象类提供外部物料系统对接模式
|
||||||
|
|
||||||
|
## 架构组成
|
||||||
|
|
||||||
|
### 1. WorkstationBase(基类)
|
||||||
|
**文件**: `workstation_base.py`
|
||||||
|
|
||||||
|
**核心功能**:
|
||||||
|
- 使用deck_config和children通过`resource_ulab_to_plr`转换为PLR物料self.deck
|
||||||
|
- 基础的资源查找和管理功能
|
||||||
|
- 抽象的工作流执行接口
|
||||||
|
- ResourceSynchronizer集成点
|
||||||
|
|
||||||
|
**关键代码**:
|
||||||
|
```python
|
||||||
|
def _initialize_material_system(self, deck_config: Dict[str, Any], children_config: Dict[str, Any] = None):
|
||||||
|
"""初始化基于PLR的物料系统"""
|
||||||
|
# 合并deck_config和children
|
||||||
|
complete_config = self._merge_deck_and_children_config(deck_config, children_config)
|
||||||
|
|
||||||
|
# 使用graphio转换函数转换为PLR资源
|
||||||
|
self.deck = resource_ulab_to_plr(complete_config)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. ResourceSynchronizer(外部系统集成抽象类)
|
||||||
|
**定义在**: `workstation_base.py`
|
||||||
|
|
||||||
|
**设计目的**:
|
||||||
|
- 提供外部物料系统(如Bioyong、LIMS等)集成的标准接口
|
||||||
|
- 双向同步:从外部系统同步到本地deck,以及将本地变更同步到外部系统
|
||||||
|
- 处理外部系统的变更通知
|
||||||
|
|
||||||
|
**核心方法**:
|
||||||
|
```python
|
||||||
|
async def sync_from_external(self) -> bool:
|
||||||
|
"""从外部系统同步物料到本地deck"""
|
||||||
|
|
||||||
|
async def sync_to_external(self, plr_resource) -> bool:
|
||||||
|
"""将本地物料同步到外部系统"""
|
||||||
|
|
||||||
|
async def handle_external_change(self, change_info: Dict[str, Any]) -> bool:
|
||||||
|
"""处理外部系统的变更通知"""
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. WorkstationWithHTTP(子类示例)
|
||||||
|
**文件**: `workstation_with_http_example.py`
|
||||||
|
|
||||||
|
**扩展功能**:
|
||||||
|
- HTTP报送接收服务集成
|
||||||
|
- 具体工作流实现(液体转移、板洗等)
|
||||||
|
- Bioyong物料系统同步器示例
|
||||||
|
- 外部报送处理方法
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
### 核心依赖
|
||||||
|
- **PyLabRobot**: 物料资源管理核心(Deck, Resource, Coordinate)
|
||||||
|
- **GraphIO转换函数**: UniLab ↔ PLR格式转换
|
||||||
|
- `resource_ulab_to_plr`: UniLab格式转PLR格式
|
||||||
|
- `resource_plr_to_ulab`: PLR格式转UniLab格式
|
||||||
|
- `convert_resources_to_type`: 通用资源类型转换
|
||||||
|
- **ROS2**: 基础设备节点通信(BaseROS2DeviceNode)
|
||||||
|
|
||||||
|
### 可选依赖
|
||||||
|
- **HTTP服务**: 仅在需要外部报送接收的子类中使用
|
||||||
|
- **外部系统API**: 根据具体集成需求添加
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
### 1. 简单工作站(仅PLR物料系统)
|
||||||
|
|
||||||
|
```python
|
||||||
|
from unilabos.devices.workstation.workstation_base import WorkstationBase
|
||||||
|
|
||||||
|
# Deck配置
|
||||||
|
deck_config = {
|
||||||
|
"size_x": 1200.0,
|
||||||
|
"size_y": 800.0,
|
||||||
|
"size_z": 100.0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 子资源配置
|
||||||
|
children_config = {
|
||||||
|
"source_plate": {
|
||||||
|
"name": "source_plate",
|
||||||
|
"type": "plate",
|
||||||
|
"position": {"x": 100, "y": 100, "z": 10},
|
||||||
|
"config": {"size_x": 127.8, "size_y": 85.5, "size_z": 14.4}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 创建工作站
|
||||||
|
workstation = WorkstationBase(
|
||||||
|
device_id="simple_workstation",
|
||||||
|
deck_config=deck_config,
|
||||||
|
children_config=children_config
|
||||||
|
)
|
||||||
|
|
||||||
|
# 查找资源
|
||||||
|
plate = workstation.find_resource_by_name("source_plate")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 带HTTP服务的工作站
|
||||||
|
|
||||||
|
```python
|
||||||
|
from unilabos.devices.workstation.workstation_with_http_example import WorkstationWithHTTP
|
||||||
|
|
||||||
|
# HTTP服务配置
|
||||||
|
http_service_config = {
|
||||||
|
"enabled": True,
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 8081
|
||||||
|
}
|
||||||
|
|
||||||
|
# 创建带HTTP服务的工作站
|
||||||
|
workstation = WorkstationWithHTTP(
|
||||||
|
device_id="http_workstation",
|
||||||
|
deck_config=deck_config,
|
||||||
|
children_config=children_config,
|
||||||
|
http_service_config=http_service_config
|
||||||
|
)
|
||||||
|
|
||||||
|
# 执行工作流
|
||||||
|
success = workstation.execute_workflow("liquid_transfer", {
|
||||||
|
"volume": 100.0,
|
||||||
|
"source_wells": ["A1", "A2"],
|
||||||
|
"dest_wells": ["B1", "B2"]
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 外部系统集成
|
||||||
|
|
||||||
|
```python
|
||||||
|
class BioyongResourceSynchronizer(ResourceSynchronizer):
|
||||||
|
"""Bioyong系统同步器"""
|
||||||
|
|
||||||
|
async def sync_from_external(self) -> bool:
|
||||||
|
# 从Bioyong API获取物料
|
||||||
|
external_materials = await self._fetch_bioyong_materials()
|
||||||
|
|
||||||
|
# 转换并添加到本地deck
|
||||||
|
for material in external_materials:
|
||||||
|
await self._add_material_to_deck(material)
|
||||||
|
|
||||||
|
return True
|
||||||
|
```
|
||||||
|
|
||||||
|
## 设计优势
|
||||||
|
|
||||||
|
### 1. **简洁性**
|
||||||
|
- 基类只专注核心物料管理,没有冗余功能
|
||||||
|
- 使用成熟的PyLabRobot作为物料管理基础
|
||||||
|
|
||||||
|
### 2. **可扩展性**
|
||||||
|
- 通过子类添加HTTP服务、特定工作流等功能
|
||||||
|
- ResourceSynchronizer模式支持任意外部系统集成
|
||||||
|
|
||||||
|
### 3. **标准化**
|
||||||
|
- PLR Deck提供标准的资源管理接口
|
||||||
|
- Graphio转换函数确保格式一致性
|
||||||
|
|
||||||
|
### 4. **灵活性**
|
||||||
|
- 可选择性使用HTTP服务和外部系统集成
|
||||||
|
- 支持不同类型的工作站需求
|
||||||
|
|
||||||
|
## 发展历程
|
||||||
|
|
||||||
|
1. **初始设计**: 复杂的统一物料系统,包含HTTP服务和多种功能
|
||||||
|
2. **PyLabRobot集成**: 引入PLR Deck管理,但保留了ResourceTracker复杂性
|
||||||
|
3. **Graphio转换**: 使用graphio转换函数简化初始化
|
||||||
|
4. **最终简化**: 专注核心PLR物料系统,HTTP服务移至子类
|
||||||
|
|
||||||
|
这个架构体现了"用PyLabRobot Deck来管理物料会更好;但是要做好和外部物料系统的对接"的设计理念,以及"现在我只需要在工作站创建的时候,整体使用deck_config和children,一起通过resource_ulab_to_plr转换为plr物料self.deck即可"的简化要求。
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
from typing import Any, Dict, Optional
|
||||||
|
from pylabrobot.resources import Resource as PLRResource
|
||||||
|
from unilabos.device_comms.modbus_plc.client import ModbusTcpClient
|
||||||
|
from unilabos.devices.workstation.workstation_base import ResourceSynchronizer, WorkstationBase
|
||||||
|
|
||||||
|
|
||||||
|
class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
device_id: str,
|
||||||
|
deck_config: Dict[str, Any],
|
||||||
|
children: Optional[Dict[str, Any]] = None,
|
||||||
|
resource_synchronizer: Optional[ResourceSynchronizer] = None,
|
||||||
|
host: str = "192.168.0.0",
|
||||||
|
port: str = "",
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
device_id=device_id,
|
||||||
|
deck_config=deck_config,
|
||||||
|
children=children,
|
||||||
|
resource_synchronizer=resource_synchronizer,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.hardware_interface = ModbusTcpClient(host=host, port=port)
|
||||||
|
|
||||||
|
def run_assembly(self, wf_name: str, resource: PLRResource, params: str = "\{\}"):
|
||||||
|
"""启动工作流"""
|
||||||
|
self.current_workflow_status = WorkflowStatus.RUNNING
|
||||||
|
logger.info(f"工作站 {self.device_id} 启动工作流: {wf_name}")
|
||||||
|
|
||||||
|
# TODO: 实现工作流逻辑
|
||||||
|
|
||||||
|
anode_sheet = self.deck.get_resource("anode_sheet")
|
||||||
|
|
||||||
|
|
||||||
649
unilabos/devices/workstation/workflow_executors.py
Normal file
649
unilabos/devices/workstation/workflow_executors.py
Normal file
@@ -0,0 +1,649 @@
|
|||||||
|
"""
|
||||||
|
工作流执行器模块
|
||||||
|
Workflow Executors Module
|
||||||
|
|
||||||
|
基于单一硬件接口的工作流执行器实现
|
||||||
|
支持Modbus、HTTP、PyLabRobot和代理模式
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
from typing import Dict, Any, List, Optional, TYPE_CHECKING
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from unilabos.devices.work_station.workstation_base import WorkstationBase
|
||||||
|
|
||||||
|
from unilabos.utils.log import logger
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowExecutor(ABC):
|
||||||
|
"""工作流执行器基类 - 基于单一硬件接口"""
|
||||||
|
|
||||||
|
def __init__(self, workstation: 'WorkstationBase'):
|
||||||
|
self.workstation = workstation
|
||||||
|
self.hardware_interface = workstation.hardware_interface
|
||||||
|
self.material_management = workstation.material_management
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def execute_workflow(self, workflow_name: str, parameters: Dict[str, Any]) -> bool:
|
||||||
|
"""执行工作流"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def stop_workflow(self, emergency: bool = False) -> bool:
|
||||||
|
"""停止工作流"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def call_device(self, method: str, *args, **kwargs) -> Any:
|
||||||
|
"""调用设备方法的统一接口"""
|
||||||
|
return self.workstation.call_device_method(method, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_device_status(self) -> Dict[str, Any]:
|
||||||
|
"""获取设备状态"""
|
||||||
|
return self.workstation.get_device_status()
|
||||||
|
|
||||||
|
|
||||||
|
class ModbusWorkflowExecutor(WorkflowExecutor):
|
||||||
|
"""Modbus工作流执行器 - 适配 coin_cell_assembly_system"""
|
||||||
|
|
||||||
|
def __init__(self, workstation: 'WorkstationBase'):
|
||||||
|
super().__init__(workstation)
|
||||||
|
|
||||||
|
# 验证Modbus接口
|
||||||
|
if not (hasattr(self.hardware_interface, 'write_register') and
|
||||||
|
hasattr(self.hardware_interface, 'read_register')):
|
||||||
|
raise RuntimeError("工作站硬件接口不是有效的Modbus客户端")
|
||||||
|
|
||||||
|
def execute_workflow(self, workflow_name: str, parameters: Dict[str, Any]) -> bool:
|
||||||
|
"""执行Modbus工作流"""
|
||||||
|
if workflow_name == "battery_manufacturing":
|
||||||
|
return self._execute_battery_manufacturing(parameters)
|
||||||
|
elif workflow_name == "material_loading":
|
||||||
|
return self._execute_material_loading(parameters)
|
||||||
|
elif workflow_name == "quality_check":
|
||||||
|
return self._execute_quality_check(parameters)
|
||||||
|
else:
|
||||||
|
logger.warning(f"不支持的Modbus工作流: {workflow_name}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _execute_battery_manufacturing(self, parameters: Dict[str, Any]) -> bool:
|
||||||
|
"""执行电池制造工作流"""
|
||||||
|
try:
|
||||||
|
# 1. 物料准备检查
|
||||||
|
available_slot = self._find_available_press_slot()
|
||||||
|
if not available_slot:
|
||||||
|
raise RuntimeError("没有可用的压制槽")
|
||||||
|
|
||||||
|
logger.info(f"找到可用压制槽: {available_slot}")
|
||||||
|
|
||||||
|
# 2. 设置工艺参数(直接调用Modbus接口)
|
||||||
|
if "electrolyte_num" in parameters:
|
||||||
|
self.hardware_interface.write_register('REG_MSG_ELECTROLYTE_NUM', parameters["electrolyte_num"])
|
||||||
|
logger.info(f"设置电解液编号: {parameters['electrolyte_num']}")
|
||||||
|
|
||||||
|
if "electrolyte_volume" in parameters:
|
||||||
|
self.hardware_interface.write_register('REG_MSG_ELECTROLYTE_VOLUME',
|
||||||
|
parameters["electrolyte_volume"],
|
||||||
|
data_type="FLOAT32")
|
||||||
|
logger.info(f"设置电解液体积: {parameters['electrolyte_volume']}")
|
||||||
|
|
||||||
|
if "assembly_pressure" in parameters:
|
||||||
|
self.hardware_interface.write_register('REG_MSG_ASSEMBLY_PRESSURE',
|
||||||
|
parameters["assembly_pressure"],
|
||||||
|
data_type="FLOAT32")
|
||||||
|
logger.info(f"设置装配压力: {parameters['assembly_pressure']}")
|
||||||
|
|
||||||
|
# 3. 启动制造流程
|
||||||
|
self.hardware_interface.write_register('COIL_SYS_START_CMD', True)
|
||||||
|
logger.info("启动电池制造流程")
|
||||||
|
|
||||||
|
# 4. 确认启动成功
|
||||||
|
time.sleep(0.5)
|
||||||
|
status = self.hardware_interface.read_register('COIL_SYS_START_STATUS', count=1)
|
||||||
|
success = status[0] if status else False
|
||||||
|
|
||||||
|
if success:
|
||||||
|
logger.info(f"电池制造工作流启动成功,参数: {parameters}")
|
||||||
|
else:
|
||||||
|
logger.error("电池制造工作流启动失败")
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"执行电池制造工作流失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _execute_material_loading(self, parameters: Dict[str, Any]) -> bool:
|
||||||
|
"""执行物料装载工作流"""
|
||||||
|
try:
|
||||||
|
material_type = parameters.get('material_type', 'cathode')
|
||||||
|
position = parameters.get('position', 'A1')
|
||||||
|
|
||||||
|
logger.info(f"开始物料装载: {material_type} -> {position}")
|
||||||
|
|
||||||
|
# 设置物料类型和位置
|
||||||
|
self.hardware_interface.write_register('REG_MATERIAL_TYPE', material_type)
|
||||||
|
self.hardware_interface.write_register('REG_MATERIAL_POSITION', position)
|
||||||
|
|
||||||
|
# 启动装载
|
||||||
|
self.hardware_interface.write_register('COIL_LOAD_START', True)
|
||||||
|
|
||||||
|
# 等待装载完成
|
||||||
|
timeout = parameters.get('timeout', 30)
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while time.time() - start_time < timeout:
|
||||||
|
status = self.hardware_interface.read_register('COIL_LOAD_COMPLETE', count=1)
|
||||||
|
if status and status[0]:
|
||||||
|
logger.info(f"物料装载完成: {material_type} -> {position}")
|
||||||
|
return True
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
logger.error(f"物料装载超时: {material_type} -> {position}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"执行物料装载失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _execute_quality_check(self, parameters: Dict[str, Any]) -> bool:
|
||||||
|
"""执行质量检测工作流"""
|
||||||
|
try:
|
||||||
|
check_type = parameters.get('check_type', 'dimensional')
|
||||||
|
|
||||||
|
logger.info(f"开始质量检测: {check_type}")
|
||||||
|
|
||||||
|
# 启动质量检测
|
||||||
|
self.hardware_interface.write_register('REG_QC_TYPE', check_type)
|
||||||
|
self.hardware_interface.write_register('COIL_QC_START', True)
|
||||||
|
|
||||||
|
# 等待检测完成
|
||||||
|
timeout = parameters.get('timeout', 60)
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
while time.time() - start_time < timeout:
|
||||||
|
status = self.hardware_interface.read_register('COIL_QC_COMPLETE', count=1)
|
||||||
|
if status and status[0]:
|
||||||
|
# 读取检测结果
|
||||||
|
result = self.hardware_interface.read_register('REG_QC_RESULT', count=1)
|
||||||
|
passed = result[0] if result else False
|
||||||
|
|
||||||
|
if passed:
|
||||||
|
logger.info(f"质量检测通过: {check_type}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.warning(f"质量检测失败: {check_type}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
time.sleep(1.0)
|
||||||
|
|
||||||
|
logger.error(f"质量检测超时: {check_type}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"执行质量检测失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _find_available_press_slot(self) -> Optional[str]:
|
||||||
|
"""查找可用压制槽"""
|
||||||
|
try:
|
||||||
|
press_slots = self.material_management.find_by_category("battery_press_slot")
|
||||||
|
for slot in press_slots:
|
||||||
|
if hasattr(slot, 'has_battery') and not slot.has_battery():
|
||||||
|
return slot.name
|
||||||
|
return None
|
||||||
|
except:
|
||||||
|
# 如果物料管理系统不可用,返回默认槽位
|
||||||
|
return "A1"
|
||||||
|
|
||||||
|
def stop_workflow(self, emergency: bool = False) -> bool:
|
||||||
|
"""停止工作流"""
|
||||||
|
try:
|
||||||
|
if emergency:
|
||||||
|
self.hardware_interface.write_register('COIL_SYS_RESET_CMD', True)
|
||||||
|
logger.warning("执行紧急停止")
|
||||||
|
else:
|
||||||
|
self.hardware_interface.write_register('COIL_SYS_STOP_CMD', True)
|
||||||
|
logger.info("执行正常停止")
|
||||||
|
|
||||||
|
time.sleep(0.5)
|
||||||
|
status = self.hardware_interface.read_register('COIL_SYS_STOP_STATUS', count=1)
|
||||||
|
return status[0] if status else False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"停止Modbus工作流失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class HttpWorkflowExecutor(WorkflowExecutor):
|
||||||
|
"""HTTP工作流执行器 - 适配 reaction_station_bioyong"""
|
||||||
|
|
||||||
|
def __init__(self, workstation: 'WorkstationBase'):
|
||||||
|
super().__init__(workstation)
|
||||||
|
|
||||||
|
# 验证HTTP接口
|
||||||
|
if not (hasattr(self.hardware_interface, 'post') or
|
||||||
|
hasattr(self.hardware_interface, 'get')):
|
||||||
|
raise RuntimeError("工作站硬件接口不是有效的HTTP客户端")
|
||||||
|
|
||||||
|
def execute_workflow(self, workflow_name: str, parameters: Dict[str, Any]) -> bool:
|
||||||
|
"""执行HTTP工作流"""
|
||||||
|
try:
|
||||||
|
if workflow_name == "reaction_synthesis":
|
||||||
|
return self._execute_reaction_synthesis(parameters)
|
||||||
|
elif workflow_name == "liquid_feeding":
|
||||||
|
return self._execute_liquid_feeding(parameters)
|
||||||
|
elif workflow_name == "temperature_control":
|
||||||
|
return self._execute_temperature_control(parameters)
|
||||||
|
else:
|
||||||
|
logger.warning(f"不支持的HTTP工作流: {workflow_name}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"执行HTTP工作流失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _execute_reaction_synthesis(self, parameters: Dict[str, Any]) -> bool:
|
||||||
|
"""执行反应合成工作流"""
|
||||||
|
try:
|
||||||
|
# 1. 设置工作流序列
|
||||||
|
sequence = self._build_reaction_sequence(parameters)
|
||||||
|
self._call_rpc_method('set_workflow_sequence', json.dumps(sequence))
|
||||||
|
|
||||||
|
# 2. 设置反应参数
|
||||||
|
if parameters.get('temperature'):
|
||||||
|
self._call_rpc_method('set_temperature', parameters['temperature'])
|
||||||
|
|
||||||
|
if parameters.get('pressure'):
|
||||||
|
self._call_rpc_method('set_pressure', parameters['pressure'])
|
||||||
|
|
||||||
|
if parameters.get('stirring_speed'):
|
||||||
|
self._call_rpc_method('set_stirring_speed', parameters['stirring_speed'])
|
||||||
|
|
||||||
|
# 3. 执行工作流
|
||||||
|
result = self._call_rpc_method('execute_current_sequence', {
|
||||||
|
"task_name": "reaction_synthesis"
|
||||||
|
})
|
||||||
|
|
||||||
|
success = result.get('success', False)
|
||||||
|
if success:
|
||||||
|
logger.info("反应合成工作流执行成功")
|
||||||
|
else:
|
||||||
|
logger.error(f"反应合成工作流执行失败: {result.get('error', '未知错误')}")
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"执行反应合成工作流失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _execute_liquid_feeding(self, parameters: Dict[str, Any]) -> bool:
|
||||||
|
"""执行液体投料工作流"""
|
||||||
|
try:
|
||||||
|
reagents = parameters.get('reagents', [])
|
||||||
|
volumes = parameters.get('volumes', [])
|
||||||
|
|
||||||
|
if len(reagents) != len(volumes):
|
||||||
|
raise ValueError("试剂列表和体积列表长度不匹配")
|
||||||
|
|
||||||
|
# 执行投料序列
|
||||||
|
for reagent, volume in zip(reagents, volumes):
|
||||||
|
result = self._call_rpc_method('feed_liquid', {
|
||||||
|
'reagent': reagent,
|
||||||
|
'volume': volume
|
||||||
|
})
|
||||||
|
|
||||||
|
if not result.get('success', False):
|
||||||
|
logger.error(f"投料失败: {reagent} {volume}mL")
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.info(f"投料成功: {reagent} {volume}mL")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"执行液体投料失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _execute_temperature_control(self, parameters: Dict[str, Any]) -> bool:
|
||||||
|
"""执行温度控制工作流"""
|
||||||
|
try:
|
||||||
|
target_temp = parameters.get('temperature', 25)
|
||||||
|
hold_time = parameters.get('hold_time', 300) # 秒
|
||||||
|
|
||||||
|
# 设置目标温度
|
||||||
|
result = self._call_rpc_method('set_temperature', target_temp)
|
||||||
|
if not result.get('success', False):
|
||||||
|
logger.error(f"设置温度失败: {target_temp}°C")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 等待温度稳定
|
||||||
|
logger.info(f"等待温度稳定到 {target_temp}°C")
|
||||||
|
|
||||||
|
# 保持温度指定时间
|
||||||
|
if hold_time > 0:
|
||||||
|
logger.info(f"保持温度 {hold_time} 秒")
|
||||||
|
time.sleep(hold_time)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"执行温度控制失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _build_reaction_sequence(self, parameters: Dict[str, Any]) -> List[str]:
|
||||||
|
"""构建反应合成工作流序列"""
|
||||||
|
sequence = []
|
||||||
|
|
||||||
|
# 添加预处理步骤
|
||||||
|
if parameters.get('purge_with_inert'):
|
||||||
|
sequence.append("purge_inert_gas")
|
||||||
|
|
||||||
|
# 添加温度设置
|
||||||
|
if parameters.get('temperature'):
|
||||||
|
sequence.append(f"set_temperature_{parameters['temperature']}")
|
||||||
|
|
||||||
|
# 添加压力设置
|
||||||
|
if parameters.get('pressure'):
|
||||||
|
sequence.append(f"set_pressure_{parameters['pressure']}")
|
||||||
|
|
||||||
|
# 添加搅拌设置
|
||||||
|
if parameters.get('stirring_speed'):
|
||||||
|
sequence.append(f"set_stirring_{parameters['stirring_speed']}")
|
||||||
|
|
||||||
|
# 添加反应步骤
|
||||||
|
sequence.extend([
|
||||||
|
"start_reaction",
|
||||||
|
"monitor_progress",
|
||||||
|
"complete_reaction"
|
||||||
|
])
|
||||||
|
|
||||||
|
# 添加后处理步骤
|
||||||
|
if parameters.get('cooling_required'):
|
||||||
|
sequence.append("cool_down")
|
||||||
|
|
||||||
|
return sequence
|
||||||
|
|
||||||
|
def _call_rpc_method(self, method: str, params: Any = None) -> Dict[str, Any]:
|
||||||
|
"""调用RPC方法"""
|
||||||
|
try:
|
||||||
|
if hasattr(self.hardware_interface, method):
|
||||||
|
# 直接方法调用
|
||||||
|
if isinstance(params, dict):
|
||||||
|
params = json.dumps(params)
|
||||||
|
elif params is None:
|
||||||
|
params = ""
|
||||||
|
return getattr(self.hardware_interface, method)(params)
|
||||||
|
else:
|
||||||
|
# HTTP请求调用
|
||||||
|
if hasattr(self.hardware_interface, 'post'):
|
||||||
|
response = self.hardware_interface.post(f"/api/{method}", json=params)
|
||||||
|
return response.json()
|
||||||
|
else:
|
||||||
|
raise AttributeError(f"HTTP接口不支持方法: {method}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"调用RPC方法失败 {method}: {e}")
|
||||||
|
return {'success': False, 'error': str(e)}
|
||||||
|
|
||||||
|
def stop_workflow(self, emergency: bool = False) -> bool:
|
||||||
|
"""停止工作流"""
|
||||||
|
try:
|
||||||
|
if emergency:
|
||||||
|
result = self._call_rpc_method('scheduler_reset')
|
||||||
|
else:
|
||||||
|
result = self._call_rpc_method('scheduler_stop')
|
||||||
|
|
||||||
|
return result.get('success', False)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"停止HTTP工作流失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class PyLabRobotWorkflowExecutor(WorkflowExecutor):
|
||||||
|
"""PyLabRobot工作流执行器 - 适配 prcxi.py"""
|
||||||
|
|
||||||
|
def __init__(self, workstation: 'WorkstationBase'):
|
||||||
|
super().__init__(workstation)
|
||||||
|
|
||||||
|
# 验证PyLabRobot接口
|
||||||
|
if not (hasattr(self.hardware_interface, 'transfer_liquid') or
|
||||||
|
hasattr(self.hardware_interface, 'pickup_tips')):
|
||||||
|
raise RuntimeError("工作站硬件接口不是有效的PyLabRobot设备")
|
||||||
|
|
||||||
|
def execute_workflow(self, workflow_name: str, parameters: Dict[str, Any]) -> bool:
|
||||||
|
"""执行PyLabRobot工作流"""
|
||||||
|
try:
|
||||||
|
if workflow_name == "liquid_transfer":
|
||||||
|
return self._execute_liquid_transfer(parameters)
|
||||||
|
elif workflow_name == "tip_pickup_drop":
|
||||||
|
return self._execute_tip_operations(parameters)
|
||||||
|
elif workflow_name == "plate_handling":
|
||||||
|
return self._execute_plate_handling(parameters)
|
||||||
|
else:
|
||||||
|
logger.warning(f"不支持的PyLabRobot工作流: {workflow_name}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"执行PyLabRobot工作流失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _execute_liquid_transfer(self, parameters: Dict[str, Any]) -> bool:
|
||||||
|
"""执行液体转移工作流"""
|
||||||
|
try:
|
||||||
|
# 1. 解析物料引用
|
||||||
|
sources = self._resolve_containers(parameters.get('sources', []))
|
||||||
|
targets = self._resolve_containers(parameters.get('targets', []))
|
||||||
|
tip_racks = self._resolve_tip_racks(parameters.get('tip_racks', []))
|
||||||
|
|
||||||
|
if not sources or not targets:
|
||||||
|
raise ValueError("液体转移需要指定源容器和目标容器")
|
||||||
|
|
||||||
|
if not tip_racks:
|
||||||
|
logger.warning("未指定枪头架,将尝试自动查找")
|
||||||
|
tip_racks = self._find_available_tip_racks()
|
||||||
|
|
||||||
|
# 2. 执行液体转移
|
||||||
|
volumes = parameters.get('volumes', [])
|
||||||
|
if not volumes:
|
||||||
|
volumes = [100.0] * len(sources) # 默认体积
|
||||||
|
|
||||||
|
# 如果是同步接口
|
||||||
|
if hasattr(self.hardware_interface, 'transfer_liquid'):
|
||||||
|
result = self.hardware_interface.transfer_liquid(
|
||||||
|
sources=sources,
|
||||||
|
targets=targets,
|
||||||
|
tip_racks=tip_racks,
|
||||||
|
asp_vols=volumes,
|
||||||
|
dis_vols=volumes,
|
||||||
|
**parameters.get('options', {})
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 异步接口需要特殊处理
|
||||||
|
asyncio.run(self._async_liquid_transfer(sources, targets, tip_racks, volumes, parameters))
|
||||||
|
result = True
|
||||||
|
|
||||||
|
if result:
|
||||||
|
logger.info(f"液体转移工作流完成: {len(sources)}个源 -> {len(targets)}个目标")
|
||||||
|
|
||||||
|
return bool(result)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"执行液体转移失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def _async_liquid_transfer(self, sources, targets, tip_racks, volumes, parameters):
|
||||||
|
"""异步液体转移"""
|
||||||
|
await self.hardware_interface.transfer_liquid(
|
||||||
|
sources=sources,
|
||||||
|
targets=targets,
|
||||||
|
tip_racks=tip_racks,
|
||||||
|
asp_vols=volumes,
|
||||||
|
dis_vols=volumes,
|
||||||
|
**parameters.get('options', {})
|
||||||
|
)
|
||||||
|
|
||||||
|
def _execute_tip_operations(self, parameters: Dict[str, Any]) -> bool:
|
||||||
|
"""执行枪头操作工作流"""
|
||||||
|
try:
|
||||||
|
operation = parameters.get('operation', 'pickup')
|
||||||
|
tip_racks = self._resolve_tip_racks(parameters.get('tip_racks', []))
|
||||||
|
|
||||||
|
if not tip_racks:
|
||||||
|
raise ValueError("枪头操作需要指定枪头架")
|
||||||
|
|
||||||
|
if operation == 'pickup':
|
||||||
|
result = self.hardware_interface.pickup_tips(tip_racks[0])
|
||||||
|
logger.info("枪头拾取完成")
|
||||||
|
elif operation == 'drop':
|
||||||
|
result = self.hardware_interface.drop_tips()
|
||||||
|
logger.info("枪头丢弃完成")
|
||||||
|
else:
|
||||||
|
raise ValueError(f"不支持的枪头操作: {operation}")
|
||||||
|
|
||||||
|
return bool(result)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"执行枪头操作失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _execute_plate_handling(self, parameters: Dict[str, Any]) -> bool:
|
||||||
|
"""执行板类处理工作流"""
|
||||||
|
try:
|
||||||
|
operation = parameters.get('operation', 'move')
|
||||||
|
source_position = parameters.get('source_position')
|
||||||
|
target_position = parameters.get('target_position')
|
||||||
|
|
||||||
|
if operation == 'move' and source_position and target_position:
|
||||||
|
# 移动板类
|
||||||
|
result = self.hardware_interface.move_plate(source_position, target_position)
|
||||||
|
logger.info(f"板类移动完成: {source_position} -> {target_position}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"不支持的板类操作或参数不完整: {operation}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return bool(result)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"执行板类处理失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _resolve_containers(self, container_names: List[str]):
|
||||||
|
"""解析容器名称为实际容器对象"""
|
||||||
|
containers = []
|
||||||
|
for name in container_names:
|
||||||
|
try:
|
||||||
|
container = self.material_management.find_material_by_id(name)
|
||||||
|
if container:
|
||||||
|
containers.append(container)
|
||||||
|
else:
|
||||||
|
logger.warning(f"未找到容器: {name}")
|
||||||
|
except:
|
||||||
|
logger.warning(f"解析容器失败: {name}")
|
||||||
|
return containers
|
||||||
|
|
||||||
|
def _resolve_tip_racks(self, tip_rack_names: List[str]):
|
||||||
|
"""解析枪头架名称为实际对象"""
|
||||||
|
tip_racks = []
|
||||||
|
for name in tip_rack_names:
|
||||||
|
try:
|
||||||
|
tip_rack = self.material_management.find_by_category("tip_rack")
|
||||||
|
matching_racks = [rack for rack in tip_rack if rack.name == name]
|
||||||
|
if matching_racks:
|
||||||
|
tip_racks.extend(matching_racks)
|
||||||
|
else:
|
||||||
|
logger.warning(f"未找到枪头架: {name}")
|
||||||
|
except:
|
||||||
|
logger.warning(f"解析枪头架失败: {name}")
|
||||||
|
return tip_racks
|
||||||
|
|
||||||
|
def _find_available_tip_racks(self):
|
||||||
|
"""查找可用的枪头架"""
|
||||||
|
try:
|
||||||
|
tip_racks = self.material_management.find_by_category("tip_rack")
|
||||||
|
available_racks = [rack for rack in tip_racks if hasattr(rack, 'has_tips') and rack.has_tips()]
|
||||||
|
return available_racks[:1] # 返回第一个可用的枪头架
|
||||||
|
except:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def stop_workflow(self, emergency: bool = False) -> bool:
|
||||||
|
"""停止工作流"""
|
||||||
|
try:
|
||||||
|
if emergency:
|
||||||
|
if hasattr(self.hardware_interface, 'emergency_stop'):
|
||||||
|
return self.hardware_interface.emergency_stop()
|
||||||
|
else:
|
||||||
|
logger.warning("设备不支持紧急停止")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
if hasattr(self.hardware_interface, 'graceful_stop'):
|
||||||
|
return self.hardware_interface.graceful_stop()
|
||||||
|
elif hasattr(self.hardware_interface, 'stop'):
|
||||||
|
return self.hardware_interface.stop()
|
||||||
|
else:
|
||||||
|
logger.warning("设备不支持优雅停止")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"停止PyLabRobot工作流失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class ProxyWorkflowExecutor(WorkflowExecutor):
|
||||||
|
"""代理工作流执行器 - 处理代理模式的工作流"""
|
||||||
|
|
||||||
|
def __init__(self, workstation: 'WorkstationBase'):
|
||||||
|
super().__init__(workstation)
|
||||||
|
|
||||||
|
# 验证代理接口
|
||||||
|
if not isinstance(self.hardware_interface, str) or not self.hardware_interface.startswith("proxy:"):
|
||||||
|
raise RuntimeError("工作站硬件接口不是有效的代理字符串")
|
||||||
|
|
||||||
|
self.device_id = self.hardware_interface[6:] # 移除 "proxy:" 前缀
|
||||||
|
|
||||||
|
def execute_workflow(self, workflow_name: str, parameters: Dict[str, Any]) -> bool:
|
||||||
|
"""执行代理工作流"""
|
||||||
|
try:
|
||||||
|
# 通过协议节点调用目标设备的工作流
|
||||||
|
if self.workstation._workstation_node:
|
||||||
|
return self.workstation._workstation_node.call_device_method(
|
||||||
|
self.device_id, 'execute_workflow', workflow_name, parameters
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.error("代理模式需要workstation_node")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"执行代理工作流失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def stop_workflow(self, emergency: bool = False) -> bool:
|
||||||
|
"""停止代理工作流"""
|
||||||
|
try:
|
||||||
|
if self.workstation._workstation_node:
|
||||||
|
return self.workstation._workstation_node.call_device_method(
|
||||||
|
self.device_id, 'stop_workflow', emergency
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.error("代理模式需要workstation_node")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"停止代理工作流失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# 辅助函数
|
||||||
|
def get_executor_for_interface(hardware_interface) -> str:
|
||||||
|
"""根据硬件接口类型获取执行器类型名称"""
|
||||||
|
if isinstance(hardware_interface, str) and hardware_interface.startswith("proxy:"):
|
||||||
|
return "ProxyWorkflowExecutor"
|
||||||
|
elif hasattr(hardware_interface, 'write_register') and hasattr(hardware_interface, 'read_register'):
|
||||||
|
return "ModbusWorkflowExecutor"
|
||||||
|
elif hasattr(hardware_interface, 'post') or hasattr(hardware_interface, 'get'):
|
||||||
|
return "HttpWorkflowExecutor"
|
||||||
|
elif hasattr(hardware_interface, 'transfer_liquid') or hasattr(hardware_interface, 'pickup_tips'):
|
||||||
|
return "PyLabRobotWorkflowExecutor"
|
||||||
|
else:
|
||||||
|
return "UnknownExecutor"
|
||||||
489
unilabos/devices/workstation/workstation_base.py
Normal file
489
unilabos/devices/workstation/workstation_base.py
Normal file
@@ -0,0 +1,489 @@
|
|||||||
|
"""
|
||||||
|
工作站基类
|
||||||
|
Workstation Base Class - 简化版
|
||||||
|
|
||||||
|
基于PLR Deck的简化工作站架构
|
||||||
|
专注于核心物料系统和工作流管理
|
||||||
|
"""
|
||||||
|
|
||||||
|
import collections
|
||||||
|
import time
|
||||||
|
from typing import Dict, Any, List, Optional, Union
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
from pylabrobot.resources import Deck, Plate, Resource as PLRResource
|
||||||
|
|
||||||
|
from pylabrobot.resources.coordinate import Coordinate
|
||||||
|
from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode
|
||||||
|
|
||||||
|
from unilabos.utils.log import logger
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowStatus(Enum):
|
||||||
|
"""工作流状态"""
|
||||||
|
|
||||||
|
IDLE = "idle"
|
||||||
|
INITIALIZING = "initializing"
|
||||||
|
RUNNING = "running"
|
||||||
|
PAUSED = "paused"
|
||||||
|
STOPPING = "stopping"
|
||||||
|
STOPPED = "stopped"
|
||||||
|
ERROR = "error"
|
||||||
|
COMPLETED = "completed"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WorkflowInfo:
|
||||||
|
"""工作流信息"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
estimated_duration: float # 预估持续时间(秒)
|
||||||
|
required_materials: List[str] # 所需物料类型
|
||||||
|
output_product: str # 输出产品类型
|
||||||
|
parameters_schema: Dict[str, Any] # 参数架构
|
||||||
|
|
||||||
|
|
||||||
|
class WorkStationContainer(Plate):
|
||||||
|
"""
|
||||||
|
WorkStation 专用 Container 类,继承自 Plate和TipRack
|
||||||
|
注意这个物料必须通过plr_additional_res_reg.py注册到edge,才能正常序列化
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
size_x: float,
|
||||||
|
size_y: float,
|
||||||
|
size_z: float,
|
||||||
|
category: str,
|
||||||
|
ordering: collections.OrderedDict,
|
||||||
|
model: Optional[str] = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
这里的初始化入参要和plr的保持一致
|
||||||
|
"""
|
||||||
|
super().__init__(name, size_x, size_y, size_z, category=category, ordering=ordering, model=model)
|
||||||
|
self._unilabos_state = {} # 必须有此行,自己的类描述的是物料的
|
||||||
|
|
||||||
|
def load_state(self, state: Dict[str, Any]) -> None:
|
||||||
|
"""从给定的状态加载工作台信息。"""
|
||||||
|
super().load_state(state)
|
||||||
|
self._unilabos_state = state
|
||||||
|
|
||||||
|
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
|
||||||
|
data = super().serialize_state()
|
||||||
|
data.update(
|
||||||
|
self._unilabos_state
|
||||||
|
) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def get_workstation_plate_resource(name: str) -> PLRResource: # 要给定一个返回plr的方法
|
||||||
|
"""
|
||||||
|
用于获取一些模板,例如返回一个带有特定信息/子物料的 Plate,这里需要到注册表注册,例如unilabos/registry/resources/organic/workstation.yaml
|
||||||
|
可以直接运行该函数或者利用注册表补全机制,来检查是否资源出错
|
||||||
|
:param name: 资源名称
|
||||||
|
:return: Resource对象
|
||||||
|
"""
|
||||||
|
plate = WorkStationContainer(
|
||||||
|
name, size_x=50, size_y=50, size_z=10, category="plate", ordering=collections.OrderedDict()
|
||||||
|
)
|
||||||
|
tip_rack = WorkStationContainer(
|
||||||
|
"tip_rack_inside_plate",
|
||||||
|
size_x=50,
|
||||||
|
size_y=50,
|
||||||
|
size_z=10,
|
||||||
|
category="tip_rack",
|
||||||
|
ordering=collections.OrderedDict(),
|
||||||
|
)
|
||||||
|
plate.assign_child_resource(tip_rack, Coordinate.zero())
|
||||||
|
return plate
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceSynchronizer(ABC):
|
||||||
|
"""资源同步器基类
|
||||||
|
|
||||||
|
负责与外部物料系统的同步,并对 self.deck 做修改
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, workstation: "WorkstationBase"):
|
||||||
|
self.workstation = workstation
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def sync_from_external(self) -> bool:
|
||||||
|
"""从外部系统同步物料到本地deck"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def sync_to_external(self, plr_resource: PLRResource) -> bool:
|
||||||
|
"""将本地物料同步到外部系统"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def handle_external_change(self, change_info: Dict[str, Any]) -> bool:
|
||||||
|
"""处理外部系统的变更通知"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WorkstationBase(ABC):
|
||||||
|
"""工作站基类 - 简化版
|
||||||
|
|
||||||
|
核心功能:
|
||||||
|
1. 基于 PLR Deck 的物料系统,支持格式转换
|
||||||
|
2. 可选的资源同步器支持外部物料系统
|
||||||
|
3. 简化的工作流管理
|
||||||
|
"""
|
||||||
|
|
||||||
|
_ros_node: ROS2WorkstationNode
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _children(self) -> Dict[str, Any]: # 不要删除这个下划线,不然会自动导入注册表,后面改成装饰器识别
|
||||||
|
return self._ros_node.children
|
||||||
|
|
||||||
|
async def update_resource_example(self):
|
||||||
|
return await self._ros_node.update_resource([get_workstation_plate_resource("test")])
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
station_resource: PLRResource,
|
||||||
|
*args,
|
||||||
|
**kwargs, # 必须有kwargs
|
||||||
|
):
|
||||||
|
# 基本配置
|
||||||
|
print(station_resource)
|
||||||
|
self.deck_config = station_resource
|
||||||
|
|
||||||
|
# PLR 物料系统
|
||||||
|
self.deck: Optional[Deck] = None
|
||||||
|
self.plr_resources: Dict[str, PLRResource] = {}
|
||||||
|
|
||||||
|
# 资源同步器(可选)
|
||||||
|
self.resource_synchronizer = ResourceSynchronizer(self) # 要在driver中自行初始化,只有workstation用
|
||||||
|
|
||||||
|
# 硬件接口
|
||||||
|
self.hardware_interface: Union[Any, str] = None
|
||||||
|
|
||||||
|
# 工作流状态
|
||||||
|
self.current_workflow_status = WorkflowStatus.IDLE
|
||||||
|
self.current_workflow_info = None
|
||||||
|
self.workflow_start_time = None
|
||||||
|
self.workflow_parameters = {}
|
||||||
|
|
||||||
|
# 支持的工作流(静态预定义)
|
||||||
|
self.supported_workflows: Dict[str, WorkflowInfo] = {}
|
||||||
|
|
||||||
|
# 初始化物料系统
|
||||||
|
self._initialize_material_system()
|
||||||
|
|
||||||
|
# 注册支持的工作流
|
||||||
|
self._register_supported_workflows()
|
||||||
|
|
||||||
|
logger.info(f"工作站 {device_id} 初始化完成(简化版)")
|
||||||
|
|
||||||
|
def _initialize_material_system(self):
|
||||||
|
"""初始化物料系统 - 使用 graphio 转换"""
|
||||||
|
try:
|
||||||
|
from unilabos.resources.graphio import resource_ulab_to_plr
|
||||||
|
|
||||||
|
# 1. 合并 deck_config 和 children 创建完整的资源树
|
||||||
|
complete_resource_config = self._create_complete_resource_config()
|
||||||
|
|
||||||
|
# 2. 使用 graphio 转换为 PLR 资源
|
||||||
|
self.deck = resource_ulab_to_plr(complete_resource_config, plr_model=True)
|
||||||
|
|
||||||
|
# 3. 建立资源映射
|
||||||
|
self._build_resource_mappings(self.deck)
|
||||||
|
|
||||||
|
# 4. 如果有资源同步器,执行初始同步
|
||||||
|
if self.resource_synchronizer:
|
||||||
|
# 这里可以异步执行,暂时跳过
|
||||||
|
pass
|
||||||
|
|
||||||
|
logger.info(f"工作站 {self.device_id} 物料系统初始化成功,创建了 {len(self.plr_resources)} 个资源")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"工作站 {self.device_id} 物料系统初始化失败: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _create_complete_resource_config(self) -> Dict[str, Any]:
|
||||||
|
"""创建完整的资源配置 - 合并 deck_config 和 children"""
|
||||||
|
# 创建主 deck 配置
|
||||||
|
deck_resource = {
|
||||||
|
"id": f"{self.device_id}_deck",
|
||||||
|
"name": f"{self.device_id}_deck",
|
||||||
|
"type": "deck",
|
||||||
|
"position": {"x": 0, "y": 0, "z": 0},
|
||||||
|
"config": {
|
||||||
|
"size_x": self.deck_config.get("size_x", 1000.0),
|
||||||
|
"size_y": self.deck_config.get("size_y", 1000.0),
|
||||||
|
"size_z": self.deck_config.get("size_z", 100.0),
|
||||||
|
**{k: v for k, v in self.deck_config.items() if k not in ["size_x", "size_y", "size_z"]},
|
||||||
|
},
|
||||||
|
"data": {},
|
||||||
|
"children": [],
|
||||||
|
"parent": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 添加子资源
|
||||||
|
if self._children:
|
||||||
|
children_list = []
|
||||||
|
for child_id, child_config in self._children.items():
|
||||||
|
child_resource = self._normalize_child_resource(child_id, child_config, deck_resource["id"])
|
||||||
|
children_list.append(child_resource)
|
||||||
|
deck_resource["children"] = children_list
|
||||||
|
|
||||||
|
return deck_resource
|
||||||
|
|
||||||
|
def _normalize_child_resource(self, resource_id: str, config: Dict[str, Any], parent_id: str) -> Dict[str, Any]:
|
||||||
|
"""标准化子资源配置"""
|
||||||
|
return {
|
||||||
|
"id": resource_id,
|
||||||
|
"name": config.get("name", resource_id),
|
||||||
|
"type": config.get("type", "container"),
|
||||||
|
"position": self._normalize_position(config.get("position", {})),
|
||||||
|
"config": config.get("config", {}),
|
||||||
|
"data": config.get("data", {}),
|
||||||
|
"children": [], # 简化版本:只支持一层子资源
|
||||||
|
"parent": parent_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _normalize_position(self, position: Any) -> Dict[str, float]:
|
||||||
|
"""标准化位置信息"""
|
||||||
|
if isinstance(position, dict):
|
||||||
|
return {
|
||||||
|
"x": float(position.get("x", 0)),
|
||||||
|
"y": float(position.get("y", 0)),
|
||||||
|
"z": float(position.get("z", 0)),
|
||||||
|
}
|
||||||
|
elif isinstance(position, (list, tuple)) and len(position) >= 2:
|
||||||
|
return {
|
||||||
|
"x": float(position[0]),
|
||||||
|
"y": float(position[1]),
|
||||||
|
"z": float(position[2]) if len(position) > 2 else 0.0,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {"x": 0.0, "y": 0.0, "z": 0.0}
|
||||||
|
|
||||||
|
def _build_resource_mappings(self, deck: Deck):
|
||||||
|
"""递归构建资源映射"""
|
||||||
|
|
||||||
|
def add_resource_recursive(resource: PLRResource):
|
||||||
|
if hasattr(resource, "name"):
|
||||||
|
self.plr_resources[resource.name] = resource
|
||||||
|
|
||||||
|
if hasattr(resource, "children"):
|
||||||
|
for child in resource.children:
|
||||||
|
add_resource_recursive(child)
|
||||||
|
|
||||||
|
add_resource_recursive(deck)
|
||||||
|
|
||||||
|
# ============ 硬件接口管理 ============
|
||||||
|
|
||||||
|
def set_hardware_interface(self, hardware_interface: Union[Any, str]):
|
||||||
|
"""设置硬件接口"""
|
||||||
|
self.hardware_interface = hardware_interface
|
||||||
|
logger.info(f"工作站 {self.device_id} 硬件接口设置: {type(hardware_interface).__name__}")
|
||||||
|
|
||||||
|
def set_workstation_node(self, workstation_node: "ROS2WorkstationNode"):
|
||||||
|
"""设置协议节点引用(用于代理模式)"""
|
||||||
|
self._ros_node = workstation_node
|
||||||
|
logger.info(f"工作站 {self.device_id} 关联协议节点")
|
||||||
|
|
||||||
|
# ============ 设备操作接口 ============
|
||||||
|
|
||||||
|
def call_device_method(self, method: str, *args, **kwargs) -> Any:
|
||||||
|
"""调用设备方法的统一接口"""
|
||||||
|
# 1. 代理模式:通过协议节点转发
|
||||||
|
if isinstance(self.hardware_interface, str) and self.hardware_interface.startswith("proxy:"):
|
||||||
|
if not self._ros_node:
|
||||||
|
raise RuntimeError("代理模式需要设置workstation_node")
|
||||||
|
|
||||||
|
device_id = self.hardware_interface[6:] # 移除 "proxy:" 前缀
|
||||||
|
return self._ros_node.call_device_method(device_id, method, *args, **kwargs)
|
||||||
|
|
||||||
|
# 2. 直接模式:直接调用硬件接口方法
|
||||||
|
elif self.hardware_interface and hasattr(self.hardware_interface, method):
|
||||||
|
return getattr(self.hardware_interface, method)(*args, **kwargs)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise AttributeError(f"硬件接口不支持方法: {method}")
|
||||||
|
|
||||||
|
def get_device_status(self) -> Dict[str, Any]:
|
||||||
|
"""获取设备状态"""
|
||||||
|
try:
|
||||||
|
return self.call_device_method("get_status")
|
||||||
|
except AttributeError:
|
||||||
|
# 如果设备不支持get_status方法,返回基础状态
|
||||||
|
return {
|
||||||
|
"status": "unknown",
|
||||||
|
"interface_type": type(self.hardware_interface).__name__,
|
||||||
|
"timestamp": time.time(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def is_device_available(self) -> bool:
|
||||||
|
"""检查设备是否可用"""
|
||||||
|
try:
|
||||||
|
self.get_device_status()
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ============ 物料系统接口 ============
|
||||||
|
|
||||||
|
def get_deck(self) -> Deck:
|
||||||
|
"""获取主 Deck"""
|
||||||
|
return self.deck
|
||||||
|
|
||||||
|
def get_all_resources(self) -> Dict[str, PLRResource]:
|
||||||
|
"""获取所有 PLR 资源"""
|
||||||
|
return self.plr_resources.copy()
|
||||||
|
|
||||||
|
def find_resource_by_name(self, name: str) -> Optional[PLRResource]:
|
||||||
|
"""按名称查找资源"""
|
||||||
|
return self.plr_resources.get(name)
|
||||||
|
|
||||||
|
def find_resources_by_type(self, resource_type: type) -> List[PLRResource]:
|
||||||
|
"""按类型查找资源"""
|
||||||
|
return [res for res in self.plr_resources.values() if isinstance(res, resource_type)]
|
||||||
|
|
||||||
|
async def sync_with_external_system(self) -> bool:
|
||||||
|
"""与外部物料系统同步"""
|
||||||
|
if not self.resource_synchronizer:
|
||||||
|
logger.info(f"工作站 {self.device_id} 没有配置资源同步器")
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
success = await self.resource_synchronizer.sync_from_external()
|
||||||
|
if success:
|
||||||
|
logger.info(f"工作站 {self.device_id} 外部同步成功")
|
||||||
|
else:
|
||||||
|
logger.warning(f"工作站 {self.device_id} 外部同步失败")
|
||||||
|
return success
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"工作站 {self.device_id} 外部同步异常: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ============ 简化的工作流控制 ============
|
||||||
|
|
||||||
|
def execute_workflow(self, workflow_name: str, parameters: Dict[str, Any]) -> bool:
|
||||||
|
"""执行工作流"""
|
||||||
|
try:
|
||||||
|
# 设置工作流状态
|
||||||
|
self.current_workflow_status = WorkflowStatus.INITIALIZING
|
||||||
|
self.workflow_parameters = parameters
|
||||||
|
self.workflow_start_time = time.time()
|
||||||
|
|
||||||
|
# 委托给子类实现
|
||||||
|
success = self._execute_workflow_impl(workflow_name, parameters)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.current_workflow_status = WorkflowStatus.RUNNING
|
||||||
|
logger.info(f"工作站 {self.device_id} 工作流 {workflow_name} 启动成功")
|
||||||
|
else:
|
||||||
|
self.current_workflow_status = WorkflowStatus.ERROR
|
||||||
|
logger.error(f"工作站 {self.device_id} 工作流 {workflow_name} 启动失败")
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.current_workflow_status = WorkflowStatus.ERROR
|
||||||
|
logger.error(f"工作站 {self.device_id} 执行工作流失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def stop_workflow(self, emergency: bool = False) -> bool:
|
||||||
|
"""停止工作流"""
|
||||||
|
try:
|
||||||
|
if self.current_workflow_status in [WorkflowStatus.IDLE, WorkflowStatus.STOPPED]:
|
||||||
|
logger.warning(f"工作站 {self.device_id} 没有正在运行的工作流")
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.current_workflow_status = WorkflowStatus.STOPPING
|
||||||
|
|
||||||
|
# 委托给子类实现
|
||||||
|
success = self._stop_workflow_impl(emergency)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.current_workflow_status = WorkflowStatus.STOPPED
|
||||||
|
logger.info(f"工作站 {self.device_id} 工作流停止成功 (紧急: {emergency})")
|
||||||
|
else:
|
||||||
|
self.current_workflow_status = WorkflowStatus.ERROR
|
||||||
|
logger.error(f"工作站 {self.device_id} 工作流停止失败")
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.current_workflow_status = WorkflowStatus.ERROR
|
||||||
|
logger.error(f"工作站 {self.device_id} 停止工作流失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ============ 状态属性 ============
|
||||||
|
|
||||||
|
@property
|
||||||
|
def workflow_status(self) -> WorkflowStatus:
|
||||||
|
"""获取当前工作流状态"""
|
||||||
|
return self.current_workflow_status
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_busy(self) -> bool:
|
||||||
|
"""检查工作站是否忙碌"""
|
||||||
|
return self.current_workflow_status in [
|
||||||
|
WorkflowStatus.INITIALIZING,
|
||||||
|
WorkflowStatus.RUNNING,
|
||||||
|
WorkflowStatus.STOPPING,
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def workflow_runtime(self) -> float:
|
||||||
|
"""获取工作流运行时间(秒)"""
|
||||||
|
if self.workflow_start_time is None:
|
||||||
|
return 0.0
|
||||||
|
return time.time() - self.workflow_start_time
|
||||||
|
|
||||||
|
# ============ 抽象方法 - 子类必须实现 ============
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _register_supported_workflows(self):
|
||||||
|
"""注册支持的工作流 - 子类必须实现"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _execute_workflow_impl(self, workflow_name: str, parameters: Dict[str, Any]) -> bool:
|
||||||
|
"""执行工作流的具体实现 - 子类必须实现"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _stop_workflow_impl(self, emergency: bool = False) -> bool:
|
||||||
|
"""停止工作流的具体实现 - 子类必须实现"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class WorkstationExample(WorkstationBase):
|
||||||
|
"""工作站示例实现"""
|
||||||
|
|
||||||
|
def _register_supported_workflows(self):
|
||||||
|
"""注册支持的工作流"""
|
||||||
|
self.supported_workflows["example_workflow"] = WorkflowInfo(
|
||||||
|
name="example_workflow",
|
||||||
|
description="这是一个示例工作流",
|
||||||
|
estimated_duration=300.0,
|
||||||
|
required_materials=["sample_plate"],
|
||||||
|
output_product="processed_plate",
|
||||||
|
parameters_schema={"param1": "string", "param2": "integer"},
|
||||||
|
)
|
||||||
|
|
||||||
|
def _execute_workflow_impl(self, workflow_name: str, parameters: Dict[str, Any]) -> bool:
|
||||||
|
"""执行工作流的具体实现"""
|
||||||
|
if workflow_name not in self.supported_workflows:
|
||||||
|
logger.error(f"工作站 {self.device_id} 不支持工作流: {workflow_name}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 这里添加实际的工作流逻辑
|
||||||
|
logger.info(f"工作站 {self.device_id} 正在执行工作流: {workflow_name} with parameters {parameters}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _stop_workflow_impl(self, emergency: bool = False) -> bool:
|
||||||
|
"""停止工作流的具体实现"""
|
||||||
|
# 这里添加实际的停止逻辑
|
||||||
|
logger.info(f"工作站 {self.device_id} 正在停止工作流 (紧急: {emergency})")
|
||||||
|
return True
|
||||||
605
unilabos/devices/workstation/workstation_http_service.py
Normal file
605
unilabos/devices/workstation/workstation_http_service.py
Normal file
@@ -0,0 +1,605 @@
|
|||||||
|
"""
|
||||||
|
工作站HTTP服务模块
|
||||||
|
Workstation HTTP Service Module
|
||||||
|
|
||||||
|
统一的工作站报送接收服务,基于LIMS协议规范:
|
||||||
|
1. 步骤完成报送 - POST /report/step_finish
|
||||||
|
2. 通量完成报送 - POST /report/sample_finish
|
||||||
|
3. 任务完成报送 - POST /report/order_finish
|
||||||
|
4. 批量更新报送 - POST /report/batch_update
|
||||||
|
5. 物料变更报送 - POST /report/material_change
|
||||||
|
6. 错误处理报送 - POST /report/error_handling
|
||||||
|
7. 健康检查和状态查询
|
||||||
|
|
||||||
|
统一使用LIMS协议字段规范,简化接口避免功能重复
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
from typing import Dict, Any, Optional, List
|
||||||
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
from dataclasses import dataclass, asdict
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from unilabos.utils.log import logger
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WorkstationReportRequest:
|
||||||
|
"""统一工作站报送请求(基于LIMS协议规范)"""
|
||||||
|
token: str # 授权令牌
|
||||||
|
request_time: str # 请求时间,格式:2024-12-12 12:12:12.xxx
|
||||||
|
data: Dict[str, Any] # 报送数据
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MaterialUsage:
|
||||||
|
"""物料使用记录"""
|
||||||
|
materialId: str # 物料Id(GUID)
|
||||||
|
locationId: str # 库位Id(GUID)
|
||||||
|
typeMode: str # 物料类型(样品1、试剂2、耗材0)
|
||||||
|
usedQuantity: float # 使用的数量(数字)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class HttpResponse:
|
||||||
|
"""HTTP响应"""
|
||||||
|
success: bool
|
||||||
|
message: str
|
||||||
|
data: Optional[Dict[str, Any]] = None
|
||||||
|
acknowledgment_id: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
||||||
|
"""工作站HTTP请求处理器"""
|
||||||
|
|
||||||
|
def __init__(self, workstation_instance, *args, **kwargs):
|
||||||
|
self.workstation = workstation_instance
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
"""处理POST请求 - 统一的工作站报送接口"""
|
||||||
|
try:
|
||||||
|
# 解析请求路径
|
||||||
|
parsed_path = urlparse(self.path)
|
||||||
|
endpoint = parsed_path.path
|
||||||
|
|
||||||
|
# 读取请求体
|
||||||
|
content_length = int(self.headers.get('Content-Length', 0))
|
||||||
|
if content_length > 0:
|
||||||
|
post_data = self.rfile.read(content_length)
|
||||||
|
request_data = json.loads(post_data.decode('utf-8'))
|
||||||
|
else:
|
||||||
|
request_data = {}
|
||||||
|
|
||||||
|
logger.info(f"收到工作站报送: {endpoint} - {request_data.get('token', 'unknown')}")
|
||||||
|
|
||||||
|
# 统一的报送端点路由(基于LIMS协议规范)
|
||||||
|
if endpoint == '/report/step_finish':
|
||||||
|
response = self._handle_step_finish_report(request_data)
|
||||||
|
elif endpoint == '/report/sample_finish':
|
||||||
|
response = self._handle_sample_finish_report(request_data)
|
||||||
|
elif endpoint == '/report/order_finish':
|
||||||
|
response = self._handle_order_finish_report(request_data)
|
||||||
|
elif endpoint == '/report/batch_update':
|
||||||
|
response = self._handle_batch_update_report(request_data)
|
||||||
|
# 扩展报送端点
|
||||||
|
elif endpoint == '/report/material_change':
|
||||||
|
response = self._handle_material_change_report(request_data)
|
||||||
|
elif endpoint == '/report/error_handling':
|
||||||
|
response = self._handle_error_handling_report(request_data)
|
||||||
|
# 保留LIMS协议端点以兼容现有系统
|
||||||
|
elif endpoint == '/LIMS/step_finish':
|
||||||
|
response = self._handle_step_finish_report(request_data)
|
||||||
|
elif endpoint == '/LIMS/preintake_finish':
|
||||||
|
response = self._handle_sample_finish_report(request_data)
|
||||||
|
elif endpoint == '/LIMS/order_finish':
|
||||||
|
response = self._handle_order_finish_report(request_data)
|
||||||
|
else:
|
||||||
|
response = HttpResponse(
|
||||||
|
success=False,
|
||||||
|
message=f"不支持的报送端点: {endpoint}",
|
||||||
|
data={"supported_endpoints": [
|
||||||
|
"/report/step_finish",
|
||||||
|
"/report/sample_finish",
|
||||||
|
"/report/order_finish",
|
||||||
|
"/report/batch_update",
|
||||||
|
"/report/material_change",
|
||||||
|
"/report/error_handling"
|
||||||
|
]}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 发送响应
|
||||||
|
self._send_response(response)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"处理工作站报送失败: {e}\\n{traceback.format_exc()}")
|
||||||
|
error_response = HttpResponse(
|
||||||
|
success=False,
|
||||||
|
message=f"请求处理失败: {str(e)}"
|
||||||
|
)
|
||||||
|
self._send_response(error_response)
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
"""处理GET请求 - 健康检查和状态查询"""
|
||||||
|
try:
|
||||||
|
parsed_path = urlparse(self.path)
|
||||||
|
endpoint = parsed_path.path
|
||||||
|
|
||||||
|
if endpoint == '/status':
|
||||||
|
response = self._handle_status_check()
|
||||||
|
elif endpoint == '/health':
|
||||||
|
response = HttpResponse(success=True, message="服务健康")
|
||||||
|
else:
|
||||||
|
response = HttpResponse(
|
||||||
|
success=False,
|
||||||
|
message=f"不支持的查询端点: {endpoint}",
|
||||||
|
data={"supported_endpoints": ["/status", "/health"]}
|
||||||
|
)
|
||||||
|
|
||||||
|
self._send_response(response)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"GET请求处理失败: {e}")
|
||||||
|
error_response = HttpResponse(
|
||||||
|
success=False,
|
||||||
|
message=f"GET请求处理失败: {str(e)}"
|
||||||
|
)
|
||||||
|
self._send_response(error_response)
|
||||||
|
|
||||||
|
def _handle_step_finish_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
||||||
|
"""处理步骤完成报送(统一LIMS协议规范)"""
|
||||||
|
try:
|
||||||
|
# 验证基本字段
|
||||||
|
required_fields = ['token', 'request_time', 'data']
|
||||||
|
if missing_fields := [field for field in required_fields if field not in request_data]:
|
||||||
|
return HttpResponse(
|
||||||
|
success=False,
|
||||||
|
message=f"缺少必要字段: {', '.join(missing_fields)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 验证data字段内容
|
||||||
|
data = request_data['data']
|
||||||
|
data_required_fields = ['orderCode', 'orderName', 'stepName', 'stepId', 'sampleId', 'startTime', 'endTime']
|
||||||
|
if data_missing_fields := [field for field in data_required_fields if field not in data]:
|
||||||
|
return HttpResponse(
|
||||||
|
success=False,
|
||||||
|
message=f"data字段缺少必要内容: {', '.join(data_missing_fields)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建统一请求对象
|
||||||
|
report_request = WorkstationReportRequest(
|
||||||
|
token=request_data['token'],
|
||||||
|
request_time=request_data['request_time'],
|
||||||
|
data=data
|
||||||
|
)
|
||||||
|
|
||||||
|
# 调用工作站处理方法
|
||||||
|
result = self.workstation.process_step_finish_report(report_request)
|
||||||
|
|
||||||
|
return HttpResponse(
|
||||||
|
success=True,
|
||||||
|
message=f"步骤完成报送已处理: {data['stepName']} ({data['orderCode']})",
|
||||||
|
acknowledgment_id=f"STEP_{int(time.time() * 1000)}_{data['stepId']}",
|
||||||
|
data=result
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"处理步骤完成报送失败: {e}")
|
||||||
|
return HttpResponse(
|
||||||
|
success=False,
|
||||||
|
message=f"步骤完成报送处理失败: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _handle_sample_finish_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
||||||
|
"""处理通量完成报送(统一LIMS协议规范)"""
|
||||||
|
try:
|
||||||
|
# 验证基本字段
|
||||||
|
required_fields = ['token', 'request_time', 'data']
|
||||||
|
if missing_fields := [field for field in required_fields if field not in request_data]:
|
||||||
|
return HttpResponse(
|
||||||
|
success=False,
|
||||||
|
message=f"缺少必要字段: {', '.join(missing_fields)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 验证data字段内容
|
||||||
|
data = request_data['data']
|
||||||
|
data_required_fields = ['orderCode', 'orderName', 'sampleId', 'startTime', 'endTime', 'Status']
|
||||||
|
if data_missing_fields := [field for field in data_required_fields if field not in data]:
|
||||||
|
return HttpResponse(
|
||||||
|
success=False,
|
||||||
|
message=f"data字段缺少必要内容: {', '.join(data_missing_fields)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建统一请求对象
|
||||||
|
report_request = WorkstationReportRequest(
|
||||||
|
token=request_data['token'],
|
||||||
|
request_time=request_data['request_time'],
|
||||||
|
data=data
|
||||||
|
)
|
||||||
|
|
||||||
|
# 调用工作站处理方法
|
||||||
|
result = self.workstation.process_sample_finish_report(report_request)
|
||||||
|
|
||||||
|
status_names = {
|
||||||
|
"0": "待生产", "2": "进样", "10": "开始",
|
||||||
|
"20": "完成", "-2": "异常停止", "-3": "人工停止"
|
||||||
|
}
|
||||||
|
status_desc = status_names.get(str(data['Status']), f"状态{data['Status']}")
|
||||||
|
|
||||||
|
return HttpResponse(
|
||||||
|
success=True,
|
||||||
|
message=f"通量完成报送已处理: {data['sampleId']} ({data['orderCode']}) - {status_desc}",
|
||||||
|
acknowledgment_id=f"SAMPLE_{int(time.time() * 1000)}_{data['sampleId']}",
|
||||||
|
data=result
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"处理通量完成报送失败: {e}")
|
||||||
|
return HttpResponse(
|
||||||
|
success=False,
|
||||||
|
message=f"通量完成报送处理失败: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _handle_order_finish_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
||||||
|
"""处理任务完成报送(统一LIMS协议规范)"""
|
||||||
|
try:
|
||||||
|
# 验证基本字段
|
||||||
|
required_fields = ['token', 'request_time', 'data']
|
||||||
|
if missing_fields := [field for field in required_fields if field not in request_data]:
|
||||||
|
return HttpResponse(
|
||||||
|
success=False,
|
||||||
|
message=f"缺少必要字段: {', '.join(missing_fields)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 验证data字段内容
|
||||||
|
data = request_data['data']
|
||||||
|
data_required_fields = ['orderCode', 'orderName', 'startTime', 'endTime', 'status']
|
||||||
|
if data_missing_fields := [field for field in data_required_fields if field not in data]:
|
||||||
|
return HttpResponse(
|
||||||
|
success=False,
|
||||||
|
message=f"data字段缺少必要内容: {', '.join(data_missing_fields)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 处理物料使用记录
|
||||||
|
used_materials = []
|
||||||
|
if 'usedMaterials' in data:
|
||||||
|
for material_data in data['usedMaterials']:
|
||||||
|
material = MaterialUsage(
|
||||||
|
materialId=material_data.get('materialId', ''),
|
||||||
|
locationId=material_data.get('locationId', ''),
|
||||||
|
typeMode=material_data.get('typeMode', ''),
|
||||||
|
usedQuantity=material_data.get('usedQuantity', 0.0)
|
||||||
|
)
|
||||||
|
used_materials.append(material)
|
||||||
|
|
||||||
|
# 创建统一请求对象
|
||||||
|
report_request = WorkstationReportRequest(
|
||||||
|
token=request_data['token'],
|
||||||
|
request_time=request_data['request_time'],
|
||||||
|
data=data
|
||||||
|
)
|
||||||
|
|
||||||
|
# 调用工作站处理方法
|
||||||
|
result = self.workstation.process_order_finish_report(report_request, used_materials)
|
||||||
|
|
||||||
|
status_names = {"30": "完成", "-11": "异常停止", "-12": "人工停止"}
|
||||||
|
status_desc = status_names.get(str(data['status']), f"状态{data['status']}")
|
||||||
|
|
||||||
|
return HttpResponse(
|
||||||
|
success=True,
|
||||||
|
message=f"任务完成报送已处理: {data['orderName']} ({data['orderCode']}) - {status_desc}",
|
||||||
|
acknowledgment_id=f"ORDER_{int(time.time() * 1000)}_{data['orderCode']}",
|
||||||
|
data=result
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"处理任务完成报送失败: {e}")
|
||||||
|
return HttpResponse(
|
||||||
|
success=False,
|
||||||
|
message=f"任务完成报送处理失败: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _handle_batch_update_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
||||||
|
"""处理批量报送"""
|
||||||
|
try:
|
||||||
|
step_updates = request_data.get('step_updates', [])
|
||||||
|
sample_updates = request_data.get('sample_updates', [])
|
||||||
|
order_updates = request_data.get('order_updates', [])
|
||||||
|
|
||||||
|
results = {
|
||||||
|
'step_results': [],
|
||||||
|
'sample_results': [],
|
||||||
|
'order_results': [],
|
||||||
|
'total_processed': 0,
|
||||||
|
'total_failed': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 处理批量步骤更新
|
||||||
|
for step_data in step_updates:
|
||||||
|
try:
|
||||||
|
step_data['token'] = request_data.get('token', step_data.get('token'))
|
||||||
|
step_data['request_time'] = request_data.get('request_time', step_data.get('request_time'))
|
||||||
|
result = self._handle_step_finish_report(step_data)
|
||||||
|
results['step_results'].append(result)
|
||||||
|
if result.success:
|
||||||
|
results['total_processed'] += 1
|
||||||
|
else:
|
||||||
|
results['total_failed'] += 1
|
||||||
|
except Exception as e:
|
||||||
|
results['step_results'].append(HttpResponse(success=False, message=str(e)))
|
||||||
|
results['total_failed'] += 1
|
||||||
|
|
||||||
|
# 处理批量通量更新
|
||||||
|
for sample_data in sample_updates:
|
||||||
|
try:
|
||||||
|
sample_data['token'] = request_data.get('token', sample_data.get('token'))
|
||||||
|
sample_data['request_time'] = request_data.get('request_time', sample_data.get('request_time'))
|
||||||
|
result = self._handle_sample_finish_report(sample_data)
|
||||||
|
results['sample_results'].append(result)
|
||||||
|
if result.success:
|
||||||
|
results['total_processed'] += 1
|
||||||
|
else:
|
||||||
|
results['total_failed'] += 1
|
||||||
|
except Exception as e:
|
||||||
|
results['sample_results'].append(HttpResponse(success=False, message=str(e)))
|
||||||
|
results['total_failed'] += 1
|
||||||
|
|
||||||
|
# 处理批量任务更新
|
||||||
|
for order_data in order_updates:
|
||||||
|
try:
|
||||||
|
order_data['token'] = request_data.get('token', order_data.get('token'))
|
||||||
|
order_data['request_time'] = request_data.get('request_time', order_data.get('request_time'))
|
||||||
|
result = self._handle_order_finish_report(order_data)
|
||||||
|
results['order_results'].append(result)
|
||||||
|
if result.success:
|
||||||
|
results['total_processed'] += 1
|
||||||
|
else:
|
||||||
|
results['total_failed'] += 1
|
||||||
|
except Exception as e:
|
||||||
|
results['order_results'].append(HttpResponse(success=False, message=str(e)))
|
||||||
|
results['total_failed'] += 1
|
||||||
|
|
||||||
|
return HttpResponse(
|
||||||
|
success=results['total_failed'] == 0,
|
||||||
|
message=f"批量报送处理完成: {results['total_processed']} 成功, {results['total_failed']} 失败",
|
||||||
|
acknowledgment_id=f"BATCH_{int(time.time() * 1000)}",
|
||||||
|
data=results
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"处理批量报送失败: {e}")
|
||||||
|
return HttpResponse(
|
||||||
|
success=False,
|
||||||
|
message=f"批量报送处理失败: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _handle_material_change_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
||||||
|
"""处理物料变更报送"""
|
||||||
|
try:
|
||||||
|
# 验证必需字段
|
||||||
|
required_fields = ['workstation_id', 'timestamp', 'resource_id', 'change_type']
|
||||||
|
if missing_fields := [field for field in required_fields if field not in request_data]:
|
||||||
|
return HttpResponse(
|
||||||
|
success=False,
|
||||||
|
message=f"缺少必要字段: {', '.join(missing_fields)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 调用工作站的处理方法
|
||||||
|
result = self.workstation.process_material_change_report(request_data)
|
||||||
|
|
||||||
|
return HttpResponse(
|
||||||
|
success=True,
|
||||||
|
message=f"物料变更报送已处理: {request_data['resource_id']} ({request_data['change_type']})",
|
||||||
|
acknowledgment_id=f"MATERIAL_{int(time.time() * 1000)}_{request_data['resource_id']}",
|
||||||
|
data=result
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"处理物料变更报送失败: {e}")
|
||||||
|
return HttpResponse(
|
||||||
|
success=False,
|
||||||
|
message=f"物料变更报送处理失败: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _handle_error_handling_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
||||||
|
"""处理错误处理报送"""
|
||||||
|
try:
|
||||||
|
# 验证必需字段
|
||||||
|
required_fields = ['workstation_id', 'timestamp', 'error_type', 'error_message']
|
||||||
|
if missing_fields := [field for field in required_fields if field not in request_data]:
|
||||||
|
return HttpResponse(
|
||||||
|
success=False,
|
||||||
|
message=f"缺少必要字段: {', '.join(missing_fields)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 调用工作站的处理方法
|
||||||
|
result = self.workstation.handle_external_error(request_data)
|
||||||
|
|
||||||
|
return HttpResponse(
|
||||||
|
success=True,
|
||||||
|
message=f"错误处理报送已处理: {request_data['error_type']} - {request_data['error_message']}",
|
||||||
|
acknowledgment_id=f"ERROR_{int(time.time() * 1000)}_{request_data.get('action_id', 'unknown')}",
|
||||||
|
data=result
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"处理错误处理报送失败: {e}")
|
||||||
|
return HttpResponse(
|
||||||
|
success=False,
|
||||||
|
message=f"错误处理报送处理失败: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _handle_status_check(self) -> HttpResponse:
|
||||||
|
"""处理状态查询"""
|
||||||
|
try:
|
||||||
|
return HttpResponse(
|
||||||
|
success=True,
|
||||||
|
message="工作站报送服务正常运行",
|
||||||
|
data={
|
||||||
|
"workstation_id": self.workstation.device_id,
|
||||||
|
"service_type": "unified_reporting_service",
|
||||||
|
"uptime": time.time() - getattr(self.workstation, '_start_time', time.time()),
|
||||||
|
"reports_received": getattr(self.workstation, '_reports_received_count', 0),
|
||||||
|
"supported_endpoints": [
|
||||||
|
"POST /report/step_finish",
|
||||||
|
"POST /report/sample_finish",
|
||||||
|
"POST /report/order_finish",
|
||||||
|
"POST /report/batch_update",
|
||||||
|
"POST /report/material_change",
|
||||||
|
"POST /report/error_handling",
|
||||||
|
"GET /status",
|
||||||
|
"GET /health"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"处理状态查询失败: {e}")
|
||||||
|
return HttpResponse(
|
||||||
|
success=False,
|
||||||
|
message=f"状态查询失败: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _send_response(self, response: HttpResponse):
|
||||||
|
"""发送响应"""
|
||||||
|
try:
|
||||||
|
# 设置响应状态码
|
||||||
|
status_code = 200 if response.success else 400
|
||||||
|
self.send_response(status_code)
|
||||||
|
|
||||||
|
# 设置响应头
|
||||||
|
self.send_header('Content-Type', 'application/json; charset=utf-8')
|
||||||
|
self.send_header('Access-Control-Allow-Origin', '*')
|
||||||
|
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
||||||
|
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
# 发送响应体
|
||||||
|
response_json = json.dumps(asdict(response), ensure_ascii=False, indent=2)
|
||||||
|
self.wfile.write(response_json.encode('utf-8'))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"发送响应失败: {e}")
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
"""重写日志方法"""
|
||||||
|
logger.debug(f"HTTP请求: {format % args}")
|
||||||
|
|
||||||
|
|
||||||
|
class WorkstationHTTPService:
|
||||||
|
"""工作站HTTP服务"""
|
||||||
|
|
||||||
|
def __init__(self, workstation_instance, host: str = "127.0.0.1", port: int = 8080):
|
||||||
|
self.workstation = workstation_instance
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.server = None
|
||||||
|
self.server_thread = None
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
# 初始化统计信息
|
||||||
|
self.workstation._start_time = time.time()
|
||||||
|
self.workstation._reports_received_count = 0
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""启动HTTP服务"""
|
||||||
|
try:
|
||||||
|
# 创建处理器工厂函数
|
||||||
|
def handler_factory(*args, **kwargs):
|
||||||
|
return WorkstationHTTPHandler(self.workstation, *args, **kwargs)
|
||||||
|
|
||||||
|
# 创建HTTP服务器
|
||||||
|
self.server = HTTPServer((self.host, self.port), handler_factory)
|
||||||
|
|
||||||
|
# 在单独线程中运行服务器
|
||||||
|
self.server_thread = threading.Thread(
|
||||||
|
target=self._run_server,
|
||||||
|
daemon=True,
|
||||||
|
name=f"WorkstationHTTP-{self.workstation.device_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.running = True
|
||||||
|
self.server_thread.start()
|
||||||
|
|
||||||
|
logger.info(f"工作站HTTP报送服务已启动: http://{self.host}:{self.port}")
|
||||||
|
logger.info("统一的报送端点 (基于LIMS协议规范):")
|
||||||
|
logger.info(" - POST /report/step_finish # 步骤完成报送")
|
||||||
|
logger.info(" - POST /report/sample_finish # 通量完成报送")
|
||||||
|
logger.info(" - POST /report/order_finish # 任务完成报送")
|
||||||
|
logger.info(" - POST /report/batch_update # 批量更新报送")
|
||||||
|
logger.info("扩展报送端点:")
|
||||||
|
logger.info(" - POST /report/material_change # 物料变更报送")
|
||||||
|
logger.info(" - POST /report/error_handling # 错误处理报送")
|
||||||
|
logger.info("兼容端点:")
|
||||||
|
logger.info(" - POST /LIMS/step_finish # 兼容LIMS步骤完成")
|
||||||
|
logger.info(" - POST /LIMS/preintake_finish # 兼容LIMS通量完成")
|
||||||
|
logger.info(" - POST /LIMS/order_finish # 兼容LIMS任务完成")
|
||||||
|
logger.info("服务端点:")
|
||||||
|
logger.info(" - GET /status # 服务状态查询")
|
||||||
|
logger.info(" - GET /health # 健康检查")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"启动HTTP服务失败: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""停止HTTP服务"""
|
||||||
|
try:
|
||||||
|
if self.running and self.server:
|
||||||
|
self.running = False
|
||||||
|
self.server.shutdown()
|
||||||
|
self.server.server_close()
|
||||||
|
|
||||||
|
if self.server_thread and self.server_thread.is_alive():
|
||||||
|
self.server_thread.join(timeout=5.0)
|
||||||
|
|
||||||
|
logger.info("工作站HTTP报送服务已停止")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"停止HTTP服务失败: {e}")
|
||||||
|
|
||||||
|
def _run_server(self):
|
||||||
|
"""运行HTTP服务器"""
|
||||||
|
try:
|
||||||
|
while self.running:
|
||||||
|
self.server.handle_request()
|
||||||
|
except Exception as e:
|
||||||
|
if self.running: # 只在非正常停止时记录错误
|
||||||
|
logger.error(f"HTTP服务运行错误: {e}")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_running(self) -> bool:
|
||||||
|
"""检查服务是否正在运行"""
|
||||||
|
return self.running and self.server_thread and self.server_thread.is_alive()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def service_url(self) -> str:
|
||||||
|
"""获取服务URL"""
|
||||||
|
return f"http://{self.host}:{self.port}"
|
||||||
|
|
||||||
|
|
||||||
|
# 导出主要类 - 保持向后兼容
|
||||||
|
@dataclass
|
||||||
|
class MaterialChangeReport:
|
||||||
|
"""已废弃:物料变更报送,请使用统一的WorkstationReportRequest"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TaskExecutionReport:
|
||||||
|
"""已废弃:任务执行报送,请使用统一的WorkstationReportRequest"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# 导出列表
|
||||||
|
__all__ = [
|
||||||
|
'WorkstationReportRequest',
|
||||||
|
'MaterialUsage',
|
||||||
|
'HttpResponse',
|
||||||
|
'WorkstationHTTPService',
|
||||||
|
# 向后兼容
|
||||||
|
'MaterialChangeReport',
|
||||||
|
'TaskExecutionReport'
|
||||||
|
]
|
||||||
583
unilabos/devices/workstation/workstation_material_management.py
Normal file
583
unilabos/devices/workstation/workstation_material_management.py
Normal file
@@ -0,0 +1,583 @@
|
|||||||
|
"""
|
||||||
|
工作站物料管理基类
|
||||||
|
Workstation Material Management Base Class
|
||||||
|
|
||||||
|
基于PyLabRobot的物料管理系统
|
||||||
|
"""
|
||||||
|
from typing import Dict, Any, List, Optional, Union, Type
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
import json
|
||||||
|
|
||||||
|
from pylabrobot.resources import (
|
||||||
|
Resource as PLRResource,
|
||||||
|
Container,
|
||||||
|
Deck,
|
||||||
|
Coordinate as PLRCoordinate,
|
||||||
|
)
|
||||||
|
|
||||||
|
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
|
||||||
|
from unilabos.utils.log import logger
|
||||||
|
from unilabos.resources.graphio import resource_plr_to_ulab, resource_ulab_to_plr
|
||||||
|
|
||||||
|
|
||||||
|
class MaterialManagementBase(ABC):
|
||||||
|
"""物料管理基类
|
||||||
|
|
||||||
|
定义工作站物料管理的标准接口:
|
||||||
|
1. 物料初始化 - 根据配置创建物料资源
|
||||||
|
2. 物料追踪 - 实时跟踪物料位置和状态
|
||||||
|
3. 物料查找 - 按类型、位置、状态查找物料
|
||||||
|
4. 物料转换 - PyLabRobot与UniLab资源格式转换
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
device_id: str,
|
||||||
|
deck_config: Dict[str, Any],
|
||||||
|
resource_tracker: DeviceNodeResourceTracker,
|
||||||
|
children_config: Dict[str, Dict[str, Any]] = None
|
||||||
|
):
|
||||||
|
self.device_id = device_id
|
||||||
|
self.deck_config = deck_config
|
||||||
|
self.resource_tracker = resource_tracker
|
||||||
|
self.children_config = children_config or {}
|
||||||
|
|
||||||
|
# 创建主台面
|
||||||
|
self.plr_deck = self._create_deck()
|
||||||
|
|
||||||
|
# 扩展ResourceTracker
|
||||||
|
self._extend_resource_tracker()
|
||||||
|
|
||||||
|
# 注册deck到resource tracker
|
||||||
|
self.resource_tracker.add_resource(self.plr_deck)
|
||||||
|
|
||||||
|
# 初始化子资源
|
||||||
|
self.plr_resources = {}
|
||||||
|
self._initialize_materials()
|
||||||
|
|
||||||
|
def _create_deck(self) -> Deck:
|
||||||
|
"""创建主台面"""
|
||||||
|
return Deck(
|
||||||
|
name=f"{self.device_id}_deck",
|
||||||
|
size_x=self.deck_config.get("size_x", 1000.0),
|
||||||
|
size_y=self.deck_config.get("size_y", 1000.0),
|
||||||
|
size_z=self.deck_config.get("size_z", 500.0),
|
||||||
|
origin=PLRCoordinate(0, 0, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _extend_resource_tracker(self):
|
||||||
|
"""扩展ResourceTracker以支持PyLabRobot特定功能"""
|
||||||
|
|
||||||
|
def find_by_type(resource_type):
|
||||||
|
"""按类型查找资源"""
|
||||||
|
return self._find_resources_by_type_recursive(self.plr_deck, resource_type)
|
||||||
|
|
||||||
|
def find_by_category(category: str):
|
||||||
|
"""按类别查找资源"""
|
||||||
|
found = []
|
||||||
|
for resource in self._get_all_resources():
|
||||||
|
if hasattr(resource, 'category') and resource.category == category:
|
||||||
|
found.append(resource)
|
||||||
|
return found
|
||||||
|
|
||||||
|
def find_by_name_pattern(pattern: str):
|
||||||
|
"""按名称模式查找资源"""
|
||||||
|
import re
|
||||||
|
found = []
|
||||||
|
for resource in self._get_all_resources():
|
||||||
|
if re.search(pattern, resource.name):
|
||||||
|
found.append(resource)
|
||||||
|
return found
|
||||||
|
|
||||||
|
# 动态添加方法到resource_tracker
|
||||||
|
self.resource_tracker.find_by_type = find_by_type
|
||||||
|
self.resource_tracker.find_by_category = find_by_category
|
||||||
|
self.resource_tracker.find_by_name_pattern = find_by_name_pattern
|
||||||
|
|
||||||
|
def _find_resources_by_type_recursive(self, resource, target_type):
|
||||||
|
"""递归查找指定类型的资源"""
|
||||||
|
found = []
|
||||||
|
if isinstance(resource, target_type):
|
||||||
|
found.append(resource)
|
||||||
|
|
||||||
|
# 递归查找子资源
|
||||||
|
children = getattr(resource, "children", [])
|
||||||
|
for child in children:
|
||||||
|
found.extend(self._find_resources_by_type_recursive(child, target_type))
|
||||||
|
|
||||||
|
return found
|
||||||
|
|
||||||
|
def _get_all_resources(self) -> List[PLRResource]:
|
||||||
|
"""获取所有资源"""
|
||||||
|
all_resources = []
|
||||||
|
|
||||||
|
def collect_resources(resource):
|
||||||
|
all_resources.append(resource)
|
||||||
|
children = getattr(resource, "children", [])
|
||||||
|
for child in children:
|
||||||
|
collect_resources(child)
|
||||||
|
|
||||||
|
collect_resources(self.plr_deck)
|
||||||
|
return all_resources
|
||||||
|
|
||||||
|
def _initialize_materials(self):
|
||||||
|
"""初始化物料"""
|
||||||
|
try:
|
||||||
|
# 确定创建顺序,确保父资源先于子资源创建
|
||||||
|
creation_order = self._determine_creation_order()
|
||||||
|
|
||||||
|
# 按顺序创建资源
|
||||||
|
for resource_id in creation_order:
|
||||||
|
config = self.children_config[resource_id]
|
||||||
|
self._create_plr_resource(resource_id, config)
|
||||||
|
|
||||||
|
logger.info(f"物料管理系统初始化完成,共创建 {len(self.plr_resources)} 个资源")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"物料初始化失败: {e}")
|
||||||
|
|
||||||
|
def _determine_creation_order(self) -> List[str]:
|
||||||
|
"""确定资源创建顺序"""
|
||||||
|
order = []
|
||||||
|
visited = set()
|
||||||
|
|
||||||
|
def visit(resource_id: str):
|
||||||
|
if resource_id in visited:
|
||||||
|
return
|
||||||
|
visited.add(resource_id)
|
||||||
|
|
||||||
|
config = self.children_config.get(resource_id, {})
|
||||||
|
parent_id = config.get("parent")
|
||||||
|
|
||||||
|
# 如果有父资源,先访问父资源
|
||||||
|
if parent_id and parent_id in self.children_config:
|
||||||
|
visit(parent_id)
|
||||||
|
|
||||||
|
order.append(resource_id)
|
||||||
|
|
||||||
|
for resource_id in self.children_config:
|
||||||
|
visit(resource_id)
|
||||||
|
|
||||||
|
return order
|
||||||
|
|
||||||
|
def _create_plr_resource(self, resource_id: str, config: Dict[str, Any]):
|
||||||
|
"""创建PyLabRobot资源"""
|
||||||
|
try:
|
||||||
|
resource_type = config.get("type", "unknown")
|
||||||
|
data = config.get("data", {})
|
||||||
|
location_config = config.get("location", {})
|
||||||
|
|
||||||
|
# 创建位置坐标
|
||||||
|
location = PLRCoordinate(
|
||||||
|
x=location_config.get("x", 0.0),
|
||||||
|
y=location_config.get("y", 0.0),
|
||||||
|
z=location_config.get("z", 0.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 根据类型创建资源
|
||||||
|
resource = self._create_resource_by_type(resource_id, resource_type, config, data, location)
|
||||||
|
|
||||||
|
if resource:
|
||||||
|
# 设置父子关系
|
||||||
|
parent_id = config.get("parent")
|
||||||
|
if parent_id and parent_id in self.plr_resources:
|
||||||
|
parent_resource = self.plr_resources[parent_id]
|
||||||
|
parent_resource.assign_child_resource(resource, location)
|
||||||
|
else:
|
||||||
|
# 直接放在deck上
|
||||||
|
self.plr_deck.assign_child_resource(resource, location)
|
||||||
|
|
||||||
|
# 保存资源引用
|
||||||
|
self.plr_resources[resource_id] = resource
|
||||||
|
|
||||||
|
# 注册到resource tracker
|
||||||
|
self.resource_tracker.add_resource(resource)
|
||||||
|
|
||||||
|
logger.debug(f"创建资源成功: {resource_id} ({resource_type})")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"创建资源失败 {resource_id}: {e}")
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _create_resource_by_type(
|
||||||
|
self,
|
||||||
|
resource_id: str,
|
||||||
|
resource_type: str,
|
||||||
|
config: Dict[str, Any],
|
||||||
|
data: Dict[str, Any],
|
||||||
|
location: PLRCoordinate
|
||||||
|
) -> Optional[PLRResource]:
|
||||||
|
"""根据类型创建资源 - 子类必须实现"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ============ 物料查找接口 ============
|
||||||
|
|
||||||
|
def find_materials_by_type(self, material_type: str) -> List[PLRResource]:
|
||||||
|
"""按材料类型查找物料"""
|
||||||
|
return self.resource_tracker.find_by_category(material_type)
|
||||||
|
|
||||||
|
def find_material_by_id(self, resource_id: str) -> Optional[PLRResource]:
|
||||||
|
"""按ID查找物料"""
|
||||||
|
return self.plr_resources.get(resource_id)
|
||||||
|
|
||||||
|
def find_available_positions(self, position_type: str) -> List[PLRResource]:
|
||||||
|
"""查找可用位置"""
|
||||||
|
positions = self.resource_tracker.find_by_category(position_type)
|
||||||
|
available = []
|
||||||
|
|
||||||
|
for pos in positions:
|
||||||
|
if hasattr(pos, 'is_available') and pos.is_available():
|
||||||
|
available.append(pos)
|
||||||
|
elif hasattr(pos, 'children') and len(pos.children) == 0:
|
||||||
|
available.append(pos)
|
||||||
|
|
||||||
|
return available
|
||||||
|
|
||||||
|
def get_material_inventory(self) -> Dict[str, int]:
|
||||||
|
"""获取物料库存统计"""
|
||||||
|
inventory = {}
|
||||||
|
|
||||||
|
for resource in self._get_all_resources():
|
||||||
|
if hasattr(resource, 'category'):
|
||||||
|
category = resource.category
|
||||||
|
inventory[category] = inventory.get(category, 0) + 1
|
||||||
|
|
||||||
|
return inventory
|
||||||
|
|
||||||
|
# ============ 物料状态更新接口 ============
|
||||||
|
|
||||||
|
def update_material_location(self, material_id: str, new_location: PLRCoordinate) -> bool:
|
||||||
|
"""更新物料位置"""
|
||||||
|
try:
|
||||||
|
material = self.find_material_by_id(material_id)
|
||||||
|
if material:
|
||||||
|
material.location = new_location
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"更新物料位置失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def move_material(self, material_id: str, target_container_id: str) -> bool:
|
||||||
|
"""移动物料到目标容器"""
|
||||||
|
try:
|
||||||
|
material = self.find_material_by_id(material_id)
|
||||||
|
target = self.find_material_by_id(target_container_id)
|
||||||
|
|
||||||
|
if material and target:
|
||||||
|
# 从原位置移除
|
||||||
|
if material.parent:
|
||||||
|
material.parent.unassign_child_resource(material)
|
||||||
|
|
||||||
|
# 添加到新位置
|
||||||
|
target.assign_child_resource(material)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"移动物料失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ============ 资源转换接口 ============
|
||||||
|
|
||||||
|
def convert_to_unilab_format(self, plr_resource: PLRResource) -> Dict[str, Any]:
|
||||||
|
"""将PyLabRobot资源转换为UniLab格式"""
|
||||||
|
return resource_plr_to_ulab(plr_resource)
|
||||||
|
|
||||||
|
def convert_from_unilab_format(self, unilab_resource: Dict[str, Any]) -> PLRResource:
|
||||||
|
"""将UniLab格式转换为PyLabRobot资源"""
|
||||||
|
return resource_ulab_to_plr(unilab_resource)
|
||||||
|
|
||||||
|
def get_deck_state(self) -> Dict[str, Any]:
|
||||||
|
"""获取Deck状态"""
|
||||||
|
try:
|
||||||
|
return {
|
||||||
|
"deck_info": {
|
||||||
|
"name": self.plr_deck.name,
|
||||||
|
"size": {
|
||||||
|
"x": self.plr_deck.size_x,
|
||||||
|
"y": self.plr_deck.size_y,
|
||||||
|
"z": self.plr_deck.size_z
|
||||||
|
},
|
||||||
|
"children_count": len(self.plr_deck.children)
|
||||||
|
},
|
||||||
|
"resources": {
|
||||||
|
resource_id: self.convert_to_unilab_format(resource)
|
||||||
|
for resource_id, resource in self.plr_resources.items()
|
||||||
|
},
|
||||||
|
"inventory": self.get_material_inventory()
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取Deck状态失败: {e}")
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
# ============ 数据持久化接口 ============
|
||||||
|
|
||||||
|
def save_state_to_file(self, file_path: str) -> bool:
|
||||||
|
"""保存状态到文件"""
|
||||||
|
try:
|
||||||
|
state = self.get_deck_state()
|
||||||
|
with open(file_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(state, f, indent=2, ensure_ascii=False)
|
||||||
|
logger.info(f"状态已保存到: {file_path}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"保存状态失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def load_state_from_file(self, file_path: str) -> bool:
|
||||||
|
"""从文件加载状态"""
|
||||||
|
try:
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
state = json.load(f)
|
||||||
|
|
||||||
|
# 重新创建资源
|
||||||
|
self._recreate_resources_from_state(state)
|
||||||
|
logger.info(f"状态已从文件加载: {file_path}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"加载状态失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _recreate_resources_from_state(self, state: Dict[str, Any]):
|
||||||
|
"""从状态重新创建资源"""
|
||||||
|
# 清除现有资源
|
||||||
|
self.plr_resources.clear()
|
||||||
|
self.plr_deck.children.clear()
|
||||||
|
|
||||||
|
# 从状态重新创建
|
||||||
|
resources_data = state.get("resources", {})
|
||||||
|
for resource_id, resource_data in resources_data.items():
|
||||||
|
try:
|
||||||
|
plr_resource = self.convert_from_unilab_format(resource_data)
|
||||||
|
self.plr_resources[resource_id] = plr_resource
|
||||||
|
self.plr_deck.assign_child_resource(plr_resource)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"重新创建资源失败 {resource_id}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
class CoinCellMaterialManagement(MaterialManagementBase):
|
||||||
|
"""纽扣电池物料管理类
|
||||||
|
|
||||||
|
从 button_battery_station 抽取的物料管理功能
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _create_resource_by_type(
|
||||||
|
self,
|
||||||
|
resource_id: str,
|
||||||
|
resource_type: str,
|
||||||
|
config: Dict[str, Any],
|
||||||
|
data: Dict[str, Any],
|
||||||
|
location: PLRCoordinate
|
||||||
|
) -> Optional[PLRResource]:
|
||||||
|
"""根据类型创建纽扣电池相关资源"""
|
||||||
|
|
||||||
|
# 导入纽扣电池资源类
|
||||||
|
from unilabos.device_comms.button_battery_station import (
|
||||||
|
MaterialPlate, PlateSlot, ClipMagazine, BatteryPressSlot,
|
||||||
|
TipBox64, WasteTipBox, BottleRack, Battery, ElectrodeSheet
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if resource_type == "material_plate":
|
||||||
|
return self._create_material_plate(resource_id, config, data, location)
|
||||||
|
|
||||||
|
elif resource_type == "plate_slot":
|
||||||
|
return self._create_plate_slot(resource_id, config, data, location)
|
||||||
|
|
||||||
|
elif resource_type == "clip_magazine":
|
||||||
|
return self._create_clip_magazine(resource_id, config, data, location)
|
||||||
|
|
||||||
|
elif resource_type == "battery_press_slot":
|
||||||
|
return self._create_battery_press_slot(resource_id, config, data, location)
|
||||||
|
|
||||||
|
elif resource_type == "tip_box":
|
||||||
|
return self._create_tip_box(resource_id, config, data, location)
|
||||||
|
|
||||||
|
elif resource_type == "waste_tip_box":
|
||||||
|
return self._create_waste_tip_box(resource_id, config, data, location)
|
||||||
|
|
||||||
|
elif resource_type == "bottle_rack":
|
||||||
|
return self._create_bottle_rack(resource_id, config, data, location)
|
||||||
|
|
||||||
|
elif resource_type == "battery":
|
||||||
|
return self._create_battery(resource_id, config, data, location)
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.warning(f"未知的资源类型: {resource_type}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"创建资源失败 {resource_id} ({resource_type}): {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _create_material_plate(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate):
|
||||||
|
"""创建料板"""
|
||||||
|
from unilabos.device_comms.button_battery_station import MaterialPlate, ElectrodeSheet
|
||||||
|
|
||||||
|
plate = MaterialPlate(
|
||||||
|
name=resource_id,
|
||||||
|
size_x=config.get("size_x", 80.0),
|
||||||
|
size_y=config.get("size_y", 80.0),
|
||||||
|
size_z=config.get("size_z", 10.0),
|
||||||
|
hole_diameter=config.get("hole_diameter", 15.0),
|
||||||
|
hole_depth=config.get("hole_depth", 8.0),
|
||||||
|
hole_spacing_x=config.get("hole_spacing_x", 20.0),
|
||||||
|
hole_spacing_y=config.get("hole_spacing_y", 20.0),
|
||||||
|
number=data.get("number", "")
|
||||||
|
)
|
||||||
|
plate.location = location
|
||||||
|
|
||||||
|
# 如果有预填充的极片数据,创建极片
|
||||||
|
electrode_sheets = data.get("electrode_sheets", [])
|
||||||
|
for i, sheet_data in enumerate(electrode_sheets):
|
||||||
|
if i < len(plate.children): # 确保不超过洞位数量
|
||||||
|
hole = plate.children[i]
|
||||||
|
sheet = ElectrodeSheet(
|
||||||
|
name=f"{resource_id}_sheet_{i}",
|
||||||
|
diameter=sheet_data.get("diameter", 14.0),
|
||||||
|
thickness=sheet_data.get("thickness", 0.1),
|
||||||
|
mass=sheet_data.get("mass", 0.01),
|
||||||
|
material_type=sheet_data.get("material_type", "cathode"),
|
||||||
|
info=sheet_data.get("info", "")
|
||||||
|
)
|
||||||
|
hole.place_electrode_sheet(sheet)
|
||||||
|
|
||||||
|
return plate
|
||||||
|
|
||||||
|
def _create_plate_slot(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate):
|
||||||
|
"""创建板槽位"""
|
||||||
|
from unilabos.device_comms.button_battery_station import PlateSlot
|
||||||
|
|
||||||
|
slot = PlateSlot(
|
||||||
|
name=resource_id,
|
||||||
|
max_plates=config.get("max_plates", 8)
|
||||||
|
)
|
||||||
|
slot.location = location
|
||||||
|
return slot
|
||||||
|
|
||||||
|
def _create_clip_magazine(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate):
|
||||||
|
"""创建子弹夹"""
|
||||||
|
from unilabos.device_comms.button_battery_station import ClipMagazine
|
||||||
|
|
||||||
|
magazine = ClipMagazine(
|
||||||
|
name=resource_id,
|
||||||
|
size_x=config.get("size_x", 150.0),
|
||||||
|
size_y=config.get("size_y", 100.0),
|
||||||
|
size_z=config.get("size_z", 50.0),
|
||||||
|
hole_diameter=config.get("hole_diameter", 15.0),
|
||||||
|
hole_depth=config.get("hole_depth", 40.0),
|
||||||
|
hole_spacing=config.get("hole_spacing", 25.0),
|
||||||
|
max_sheets_per_hole=config.get("max_sheets_per_hole", 100)
|
||||||
|
)
|
||||||
|
magazine.location = location
|
||||||
|
return magazine
|
||||||
|
|
||||||
|
def _create_battery_press_slot(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate):
|
||||||
|
"""创建电池压制槽"""
|
||||||
|
from unilabos.device_comms.button_battery_station import BatteryPressSlot
|
||||||
|
|
||||||
|
slot = BatteryPressSlot(
|
||||||
|
name=resource_id,
|
||||||
|
diameter=config.get("diameter", 20.0),
|
||||||
|
depth=config.get("depth", 15.0)
|
||||||
|
)
|
||||||
|
slot.location = location
|
||||||
|
return slot
|
||||||
|
|
||||||
|
def _create_tip_box(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate):
|
||||||
|
"""创建枪头盒"""
|
||||||
|
from unilabos.device_comms.button_battery_station import TipBox64
|
||||||
|
|
||||||
|
tip_box = TipBox64(
|
||||||
|
name=resource_id,
|
||||||
|
size_x=config.get("size_x", 127.8),
|
||||||
|
size_y=config.get("size_y", 85.5),
|
||||||
|
size_z=config.get("size_z", 60.0),
|
||||||
|
with_tips=data.get("with_tips", True)
|
||||||
|
)
|
||||||
|
tip_box.location = location
|
||||||
|
return tip_box
|
||||||
|
|
||||||
|
def _create_waste_tip_box(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate):
|
||||||
|
"""创建废枪头盒"""
|
||||||
|
from unilabos.device_comms.button_battery_station import WasteTipBox
|
||||||
|
|
||||||
|
waste_box = WasteTipBox(
|
||||||
|
name=resource_id,
|
||||||
|
size_x=config.get("size_x", 127.8),
|
||||||
|
size_y=config.get("size_y", 85.5),
|
||||||
|
size_z=config.get("size_z", 60.0),
|
||||||
|
max_tips=config.get("max_tips", 100)
|
||||||
|
)
|
||||||
|
waste_box.location = location
|
||||||
|
return waste_box
|
||||||
|
|
||||||
|
def _create_bottle_rack(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate):
|
||||||
|
"""创建瓶架"""
|
||||||
|
from unilabos.device_comms.button_battery_station import BottleRack
|
||||||
|
|
||||||
|
rack = BottleRack(
|
||||||
|
name=resource_id,
|
||||||
|
size_x=config.get("size_x", 210.0),
|
||||||
|
size_y=config.get("size_y", 140.0),
|
||||||
|
size_z=config.get("size_z", 100.0),
|
||||||
|
bottle_diameter=config.get("bottle_diameter", 30.0),
|
||||||
|
bottle_height=config.get("bottle_height", 100.0),
|
||||||
|
position_spacing=config.get("position_spacing", 35.0)
|
||||||
|
)
|
||||||
|
rack.location = location
|
||||||
|
return rack
|
||||||
|
|
||||||
|
def _create_battery(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate):
|
||||||
|
"""创建电池"""
|
||||||
|
from unilabos.device_comms.button_battery_station import Battery
|
||||||
|
|
||||||
|
battery = Battery(
|
||||||
|
name=resource_id,
|
||||||
|
diameter=config.get("diameter", 20.0),
|
||||||
|
height=config.get("height", 3.2),
|
||||||
|
max_volume=config.get("max_volume", 100.0),
|
||||||
|
barcode=data.get("barcode", "")
|
||||||
|
)
|
||||||
|
battery.location = location
|
||||||
|
return battery
|
||||||
|
|
||||||
|
# ============ 纽扣电池特定查找方法 ============
|
||||||
|
|
||||||
|
def find_material_plates(self):
|
||||||
|
"""查找所有料板"""
|
||||||
|
from unilabos.device_comms.button_battery_station import MaterialPlate
|
||||||
|
return self.resource_tracker.find_by_type(MaterialPlate)
|
||||||
|
|
||||||
|
def find_batteries(self):
|
||||||
|
"""查找所有电池"""
|
||||||
|
from unilabos.device_comms.button_battery_station import Battery
|
||||||
|
return self.resource_tracker.find_by_type(Battery)
|
||||||
|
|
||||||
|
def find_electrode_sheets(self):
|
||||||
|
"""查找所有极片"""
|
||||||
|
found = []
|
||||||
|
plates = self.find_material_plates()
|
||||||
|
for plate in plates:
|
||||||
|
for hole in plate.children:
|
||||||
|
if hasattr(hole, 'has_electrode_sheet') and hole.has_electrode_sheet():
|
||||||
|
found.append(hole._electrode_sheet)
|
||||||
|
return found
|
||||||
|
|
||||||
|
def find_plate_slots(self):
|
||||||
|
"""查找所有板槽位"""
|
||||||
|
from unilabos.device_comms.button_battery_station import PlateSlot
|
||||||
|
return self.resource_tracker.find_by_type(PlateSlot)
|
||||||
|
|
||||||
|
def find_clip_magazines(self):
|
||||||
|
"""查找所有子弹夹"""
|
||||||
|
from unilabos.device_comms.button_battery_station import ClipMagazine
|
||||||
|
return self.resource_tracker.find_by_type(ClipMagazine)
|
||||||
|
|
||||||
|
def find_press_slots(self):
|
||||||
|
"""查找所有压制槽"""
|
||||||
|
from unilabos.device_comms.button_battery_station import BatteryPressSlot
|
||||||
|
return self.resource_tracker.find_by_type(BatteryPressSlot)
|
||||||
@@ -7,13 +7,13 @@ import networkx as nx
|
|||||||
from unilabos_msgs.msg import Resource
|
from unilabos_msgs.msg import Resource
|
||||||
|
|
||||||
from unilabos.resources.container import RegularContainer
|
from unilabos.resources.container import RegularContainer
|
||||||
from unilabos.ros.msgs.message_converter import convert_from_ros_msg_with_mapping, convert_to_ros_msg
|
from unilabos.ros.msgs.message_converter import convert_to_ros_msg
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from pylabrobot.resources.resource import Resource as ResourcePLR
|
from pylabrobot.resources.resource import Resource as ResourcePLR
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
from typing import Union, get_origin, get_args
|
from typing import Union, get_origin
|
||||||
|
|
||||||
physical_setup_graph: nx.Graph = None
|
physical_setup_graph: nx.Graph = None
|
||||||
|
|
||||||
@@ -327,7 +327,7 @@ def nested_dict_to_list(nested_dict: dict) -> list[dict]: # FIXME 是tree?
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def convert_resources_to_type(
|
def convert_resources_to_type(
|
||||||
resources_list: list[dict], resource_type: type, *, plr_model: bool = False
|
resources_list: list[dict], resource_type: Union[type, list[type]], *, plr_model: bool = False
|
||||||
) -> Union[list[dict], dict, None, "ResourcePLR"]:
|
) -> Union[list[dict], dict, None, "ResourcePLR"]:
|
||||||
"""
|
"""
|
||||||
Convert resources to a given type (PyLabRobot or NestedDict) from flattened list of dictionaries.
|
Convert resources to a given type (PyLabRobot or NestedDict) from flattened list of dictionaries.
|
||||||
@@ -358,7 +358,7 @@ def convert_resources_to_type(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def convert_resources_from_type(resources_list, resource_type: type) -> Union[list[dict], dict, None, "ResourcePLR"]:
|
def convert_resources_from_type(resources_list, resource_type: Union[type, list[type]], *, is_plr: bool = False) -> Union[list[dict], dict, None, "ResourcePLR"]:
|
||||||
"""
|
"""
|
||||||
Convert resources from a given type (PyLabRobot or NestedDict) to flattened list of dictionaries.
|
Convert resources from a given type (PyLabRobot or NestedDict) to flattened list of dictionaries.
|
||||||
|
|
||||||
@@ -378,7 +378,7 @@ def convert_resources_from_type(resources_list, resource_type: type) -> Union[li
|
|||||||
if all((get_origin(t) is Union) for t in resource_type):
|
if all((get_origin(t) is Union) for t in resource_type):
|
||||||
resources_tree = [resource_plr_to_ulab(r) for r in resources_list]
|
resources_tree = [resource_plr_to_ulab(r) for r in resources_list]
|
||||||
return tree_to_list(resources_tree)
|
return tree_to_list(resources_tree)
|
||||||
elif all(issubclass(t, ResourcePLR) for t in resource_type):
|
elif is_plr or all(issubclass(t, ResourcePLR) for t in resource_type):
|
||||||
resources_tree = [resource_plr_to_ulab(r) for r in resources_list]
|
resources_tree = [resource_plr_to_ulab(r) for r in resources_list]
|
||||||
return tree_to_list(resources_tree)
|
return tree_to_list(resources_tree)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ SendCmd = msg_converter_manager.get_class("unilabos_msgs.action:SendCmd")
|
|||||||
imsg = msg_converter_manager.get_module("unilabos.messages")
|
imsg = msg_converter_manager.get_module("unilabos.messages")
|
||||||
Point3D = msg_converter_manager.get_class("unilabos.messages:Point3D")
|
Point3D = msg_converter_manager.get_class("unilabos.messages:Point3D")
|
||||||
|
|
||||||
|
from control_msgs.action import *
|
||||||
|
|
||||||
# 基本消息类型映射
|
# 基本消息类型映射
|
||||||
_msg_mapping: Dict[Type, Type] = {
|
_msg_mapping: Dict[Type, Type] = {
|
||||||
float: Float64,
|
float: Float64,
|
||||||
|
|||||||
@@ -518,6 +518,17 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
rclpy.get_global_executor().add_node(self)
|
rclpy.get_global_executor().add_node(self)
|
||||||
self.lab_logger().debug(f"ROS节点初始化完成")
|
self.lab_logger().debug(f"ROS节点初始化完成")
|
||||||
|
|
||||||
|
async def update_resource(self, resources: List[Any]):
|
||||||
|
r = ResourceUpdate.Request()
|
||||||
|
unique_resources = []
|
||||||
|
for resource in resources: # resource是list[ResourcePLR]
|
||||||
|
# 目前更新资源只支持传入plr的对象,后面要更新convert_resources_from_type函数
|
||||||
|
converted_list = convert_resources_from_type([resource], resource_type=[object], is_plr=True)
|
||||||
|
unique_resources.extend([convert_to_ros_msg(Resource, converted) for converted in converted_list])
|
||||||
|
r.resources = unique_resources
|
||||||
|
response = await self._resource_clients["resource_update"].call_async(r)
|
||||||
|
self.lab_logger().debug(f"资源更新结果: {response}")
|
||||||
|
|
||||||
def register_device(self):
|
def register_device(self):
|
||||||
"""向注册表中注册设备信息"""
|
"""向注册表中注册设备信息"""
|
||||||
topics_info = self._property_publishers.copy()
|
topics_info = self._property_publishers.copy()
|
||||||
@@ -947,6 +958,7 @@ class ROS2DeviceNode:
|
|||||||
self._driver_class = driver_class
|
self._driver_class = driver_class
|
||||||
self.device_config = device_config
|
self.device_config = device_config
|
||||||
self.driver_is_ros = driver_is_ros
|
self.driver_is_ros = driver_is_ros
|
||||||
|
self.driver_is_workstation = False
|
||||||
self.resource_tracker = DeviceNodeResourceTracker()
|
self.resource_tracker = DeviceNodeResourceTracker()
|
||||||
|
|
||||||
# use_pylabrobot_creator 使用 cls的包路径检测
|
# use_pylabrobot_creator 使用 cls的包路径检测
|
||||||
@@ -967,10 +979,11 @@ class ROS2DeviceNode:
|
|||||||
driver_class, children=children, resource_tracker=self.resource_tracker
|
driver_class, children=children, resource_tracker=self.resource_tracker
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
from unilabos.ros.nodes.presets.protocol_node import ROS2ProtocolNode
|
from unilabos.devices.workstation.workstation_base import WorkstationBase
|
||||||
|
|
||||||
if issubclass(self._driver_class, ROS2ProtocolNode): # 是ProtocolNode的子节点,就要调用ProtocolNodeCreator
|
if issubclass(self._driver_class, WorkstationBase): # 是WorkstationNode的子节点,就要调用WorkstationNodeCreator
|
||||||
self._driver_creator = ProtocolNodeCreator(driver_class, children=children, resource_tracker=self.resource_tracker)
|
self.driver_is_workstation = True
|
||||||
|
self._driver_creator = WorkstationNodeCreator(driver_class, children=children, resource_tracker=self.resource_tracker)
|
||||||
else:
|
else:
|
||||||
self._driver_creator = DeviceClassCreator(driver_class, children=children, resource_tracker=self.resource_tracker)
|
self._driver_creator = DeviceClassCreator(driver_class, children=children, resource_tracker=self.resource_tracker)
|
||||||
|
|
||||||
@@ -985,6 +998,19 @@ class ROS2DeviceNode:
|
|||||||
# 创建ROS2节点
|
# 创建ROS2节点
|
||||||
if driver_is_ros:
|
if driver_is_ros:
|
||||||
self._ros_node = self._driver_instance # type: ignore
|
self._ros_node = self._driver_instance # type: ignore
|
||||||
|
elif self.driver_is_workstation:
|
||||||
|
from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode
|
||||||
|
self._ros_node = ROS2WorkstationNode(
|
||||||
|
protocol_type=driver_params["protocol_type"],
|
||||||
|
children=children,
|
||||||
|
driver_instance=self._driver_instance, # type: ignore
|
||||||
|
device_id=device_id,
|
||||||
|
status_types=status_types,
|
||||||
|
action_value_mappings=action_value_mappings,
|
||||||
|
hardware_interface=hardware_interface,
|
||||||
|
print_publish=print_publish,
|
||||||
|
resource_tracker=self.resource_tracker,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self._ros_node = BaseROS2DeviceNode(
|
self._ros_node = BaseROS2DeviceNode(
|
||||||
driver_instance=self._driver_instance,
|
driver_instance=self._driver_instance,
|
||||||
|
|||||||
@@ -553,7 +553,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
|
|
||||||
# 解析设备名和属性名
|
# 解析设备名和属性名
|
||||||
parts = topic.split("/")
|
parts = topic.split("/")
|
||||||
if len(parts) >= 4: # 可能有ProtocolNode,创建更长的设备
|
if len(parts) >= 4: # 可能有WorkstationNode,创建更长的设备
|
||||||
device_id = "/".join(parts[2:-1])
|
device_id = "/".join(parts[2:-1])
|
||||||
property_name = parts[-1]
|
property_name = parts[-1]
|
||||||
|
|
||||||
|
|||||||
@@ -1,86 +1,413 @@
|
|||||||
import collections
|
import json
|
||||||
from typing import Union, Dict, Any, Optional
|
import time
|
||||||
|
import traceback
|
||||||
|
from pprint import pformat
|
||||||
|
from typing import List, Dict, Any, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from unilabos_msgs.msg import Resource
|
import rclpy
|
||||||
from pylabrobot.resources import Resource as PLRResource, Plate, TipRack, Coordinate
|
from rosidl_runtime_py import message_to_ordereddict
|
||||||
from unilabos.ros.nodes.presets.protocol_node import ROS2ProtocolNode
|
|
||||||
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
|
from unilabos.messages import * # type: ignore # protocol names
|
||||||
|
from rclpy.action import ActionServer, ActionClient
|
||||||
|
from rclpy.action.server import ServerGoalHandle
|
||||||
|
from rclpy.callback_groups import ReentrantCallbackGroup
|
||||||
|
from unilabos_msgs.msg import Resource # type: ignore
|
||||||
|
from unilabos_msgs.srv import ResourceGet, ResourceUpdate # type: ignore
|
||||||
|
|
||||||
|
from unilabos.compile import action_protocol_generators
|
||||||
|
from unilabos.resources.graphio import list_to_nested_dict, nested_dict_to_list
|
||||||
|
from unilabos.ros.initialize_device import initialize_device_from_dict
|
||||||
|
from unilabos.ros.msgs.message_converter import (
|
||||||
|
get_action_type,
|
||||||
|
convert_to_ros_msg,
|
||||||
|
convert_from_ros_msg,
|
||||||
|
convert_from_ros_msg_with_mapping,
|
||||||
|
)
|
||||||
|
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, DeviceNodeResourceTracker, ROS2DeviceNode
|
||||||
|
from unilabos.utils.type_check import serialize_result_info
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from unilabos.devices.workstation.workstation_base import WorkstationBase
|
||||||
|
|
||||||
|
|
||||||
class WorkStationContainer(Plate, TipRack):
|
class ROS2WorkstationNode(BaseROS2DeviceNode):
|
||||||
"""
|
"""
|
||||||
WorkStation 专用 Container 类,继承自 Plate和TipRack
|
ROS2WorkstationNode代表管理ROS2环境中设备通信和动作的协议节点。
|
||||||
注意这个物料必须通过plr_additional_res_reg.py注册到edge,才能正常序列化
|
它初始化设备节点,处理动作客户端,并基于指定的协议执行工作流。
|
||||||
|
它还物理上代表一组协同工作的设备,如带夹持器的机械臂,带传送带的CNC机器等。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, name: str, size_x: float, size_y: float, size_z: float, category: str, ordering: collections.OrderedDict, model: Optional[str] = None,):
|
driver_instance: "WorkstationBase"
|
||||||
"""
|
|
||||||
这里的初始化入参要和plr的保持一致
|
|
||||||
"""
|
|
||||||
super().__init__(name, size_x, size_y, size_z, category=category, ordering=ordering, model=model)
|
|
||||||
self._unilabos_state = {} # 必须有此行,自己的类描述的是物料的
|
|
||||||
|
|
||||||
def load_state(self, state: Dict[str, Any]) -> None:
|
def __init__(
|
||||||
"""从给定的状态加载工作台信息。"""
|
|
||||||
super().load_state(state)
|
|
||||||
self._unilabos_state = state
|
|
||||||
|
|
||||||
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
|
|
||||||
data = super().serialize_state()
|
|
||||||
data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def get_workstation_plate_resource(name: str) -> PLRResource: # 要给定一个返回plr的方法
|
|
||||||
"""
|
|
||||||
用于获取一些模板,例如返回一个带有特定信息/子物料的 Plate,这里需要到注册表注册,例如unilabos/registry/resources/organic/workstation.yaml
|
|
||||||
可以直接运行该函数或者利用注册表补全机制,来检查是否资源出错
|
|
||||||
:param name: 资源名称
|
|
||||||
:return: Resource对象
|
|
||||||
"""
|
|
||||||
plate = WorkStationContainer(name, size_x=50, size_y=50, size_z=10, category="plate", ordering=collections.OrderedDict())
|
|
||||||
tip_rack = WorkStationContainer("tip_rack_inside_plate", size_x=50, size_y=50, size_z=10, category="tip_rack", ordering=collections.OrderedDict())
|
|
||||||
plate.assign_child_resource(tip_rack, Coordinate.zero())
|
|
||||||
return plate
|
|
||||||
|
|
||||||
|
|
||||||
class WorkStationExample(ROS2ProtocolNode):
|
|
||||||
def __init__(self,
|
|
||||||
# 你可以在这里增加任意的参数,对应启动json填写相应的参数内容
|
|
||||||
device_id: str,
|
|
||||||
children: dict,
|
|
||||||
protocol_type: Union[str, list[str]],
|
|
||||||
resource_tracker: DeviceNodeResourceTracker
|
|
||||||
):
|
|
||||||
super().__init__(device_id, children, protocol_type, resource_tracker)
|
|
||||||
|
|
||||||
def create_resource(
|
|
||||||
self,
|
self,
|
||||||
resource_tracker: DeviceNodeResourceTracker,
|
protocol_type: List[str],
|
||||||
resources: list[Resource],
|
children: Dict[str, Any],
|
||||||
bind_parent_id: str,
|
*,
|
||||||
bind_location: dict[str, float],
|
driver_instance: "WorkstationBase",
|
||||||
liquid_input_slot: list[int],
|
device_id: str,
|
||||||
liquid_type: list[str],
|
status_types: Dict[str, Any],
|
||||||
liquid_volume: list[int],
|
action_value_mappings: Dict[str, Any],
|
||||||
slot_on_deck: int,
|
hardware_interface: Dict[str, Any],
|
||||||
) -> Dict[str, Any]:
|
print_publish=True,
|
||||||
return { # edge侧返回给前端的创建物料的结果。云端调用初始化瓶子等。执行该函数时,物料已经上报给云端,一般不需要继承使用
|
resource_tracker: Optional["DeviceNodeResourceTracker"] = None,
|
||||||
|
):
|
||||||
|
self._setup_protocol_names(protocol_type)
|
||||||
|
|
||||||
|
# 初始化非BaseROSNode的属性
|
||||||
|
self.children = children
|
||||||
|
# 初始化基类,让基类处理常规动作
|
||||||
|
super().__init__(
|
||||||
|
driver_instance=driver_instance,
|
||||||
|
device_id=device_id,
|
||||||
|
status_types=status_types,
|
||||||
|
action_value_mappings={
|
||||||
|
**action_value_mappings,
|
||||||
|
**self.protocol_action_mappings
|
||||||
|
},
|
||||||
|
hardware_interface=hardware_interface,
|
||||||
|
print_publish=print_publish,
|
||||||
|
resource_tracker=resource_tracker,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._busy = False
|
||||||
|
self.sub_devices = {}
|
||||||
|
self._action_clients = {}
|
||||||
|
|
||||||
|
# 初始化子设备
|
||||||
|
self.communication_node_id_to_instance = {}
|
||||||
|
|
||||||
|
for device_id, device_config in self.children.items():
|
||||||
|
if device_config.get("type", "device") != "device":
|
||||||
|
self.lab_logger().debug(
|
||||||
|
f"[Protocol Node] Skipping type {device_config['type']} {device_id} already existed, skipping."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
d = self.initialize_device(device_id, device_config)
|
||||||
|
except Exception as ex:
|
||||||
|
self.lab_logger().error(
|
||||||
|
f"[Protocol Node] Failed to initialize device {device_id}: {ex}\n{traceback.format_exc()}")
|
||||||
|
d = None
|
||||||
|
if d is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if "serial_" in device_id or "io_" in device_id:
|
||||||
|
self.communication_node_id_to_instance[device_id] = d
|
||||||
|
continue
|
||||||
|
|
||||||
|
for device_id, device_config in self.children.items():
|
||||||
|
if device_config.get("type", "device") != "device":
|
||||||
|
continue
|
||||||
|
# 设置硬件接口代理
|
||||||
|
if device_id not in self.sub_devices:
|
||||||
|
self.lab_logger().error(f"[Protocol Node] {device_id} 还没有正确初始化,跳过...")
|
||||||
|
continue
|
||||||
|
d = self.sub_devices[device_id]
|
||||||
|
if d:
|
||||||
|
hardware_interface = d.ros_node_instance._hardware_interface
|
||||||
|
if (
|
||||||
|
hasattr(d.driver_instance, hardware_interface["name"])
|
||||||
|
and hasattr(d.driver_instance, hardware_interface["write"])
|
||||||
|
and (
|
||||||
|
hardware_interface["read"] is None or hasattr(d.driver_instance, hardware_interface["read"]))
|
||||||
|
):
|
||||||
|
|
||||||
|
name = getattr(d.driver_instance, hardware_interface["name"])
|
||||||
|
read = hardware_interface.get("read", None)
|
||||||
|
write = hardware_interface.get("write", None)
|
||||||
|
|
||||||
|
# 如果硬件接口是字符串,通过通信设备提供
|
||||||
|
if isinstance(name, str) and name in self.sub_devices:
|
||||||
|
communicate_device = self.sub_devices[name]
|
||||||
|
communicate_hardware_info = communicate_device.ros_node_instance._hardware_interface
|
||||||
|
self._setup_hardware_proxy(d, self.sub_devices[name], read, write)
|
||||||
|
self.lab_logger().info(
|
||||||
|
f"\n通信代理:为子设备{device_id}\n "
|
||||||
|
f"添加了{read}方法(来源:{name} {communicate_hardware_info['write']}) \n "
|
||||||
|
f"添加了{write}方法(来源:{name} {communicate_hardware_info['read']})"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.lab_logger().info(f"ROS2ProtocolNode {device_id} initialized with protocols: {self.protocol_names}")
|
||||||
|
|
||||||
|
def _setup_protocol_names(self, protocol_type):
|
||||||
|
# 处理协议类型
|
||||||
|
if isinstance(protocol_type, str):
|
||||||
|
if "," not in protocol_type:
|
||||||
|
self.protocol_names = [protocol_type]
|
||||||
|
else:
|
||||||
|
self.protocol_names = [protocol.strip() for protocol in protocol_type.split(",")]
|
||||||
|
else:
|
||||||
|
self.protocol_names = protocol_type
|
||||||
|
# 准备协议相关的动作值映射
|
||||||
|
self.protocol_action_mappings = {}
|
||||||
|
for protocol_name in self.protocol_names:
|
||||||
|
protocol_type = globals()[protocol_name]
|
||||||
|
self.protocol_action_mappings[protocol_name] = get_action_type(protocol_type)
|
||||||
|
|
||||||
|
def initialize_device(self, device_id, device_config):
|
||||||
|
"""初始化设备并创建相应的动作客户端"""
|
||||||
|
# device_id_abs = f"{self.device_id}/{device_id}"
|
||||||
|
device_id_abs = f"{device_id}"
|
||||||
|
self.lab_logger().info(f"初始化子设备: {device_id_abs}")
|
||||||
|
d = self.sub_devices[device_id] = initialize_device_from_dict(device_id_abs, device_config)
|
||||||
|
|
||||||
|
# 为子设备的每个动作创建动作客户端
|
||||||
|
if d is not None and hasattr(d, "ros_node_instance"):
|
||||||
|
node = d.ros_node_instance
|
||||||
|
node.resource_tracker = self.resource_tracker # 站内应当共享资源跟踪器
|
||||||
|
for action_name, action_mapping in node._action_value_mappings.items():
|
||||||
|
if action_name.startswith("auto-") or str(action_mapping.get("type", "")).startswith(
|
||||||
|
"UniLabJsonCommand"):
|
||||||
|
continue
|
||||||
|
action_id = f"/devices/{device_id_abs}/{action_name}"
|
||||||
|
if action_id not in self._action_clients:
|
||||||
|
try:
|
||||||
|
self._action_clients[action_id] = ActionClient(
|
||||||
|
self, action_mapping["type"], action_id, callback_group=self.callback_group
|
||||||
|
)
|
||||||
|
except Exception as ex:
|
||||||
|
self.lab_logger().error(f"创建动作客户端失败: {action_id}, 错误: {ex}")
|
||||||
|
continue
|
||||||
|
self.lab_logger().trace(f"为子设备 {device_id} 创建动作客户端: {action_name}")
|
||||||
|
return d
|
||||||
|
|
||||||
|
def create_ros_action_server(self, action_name, action_value_mapping):
|
||||||
|
"""创建ROS动作服务器"""
|
||||||
|
if action_name not in self.protocol_names:
|
||||||
|
# 非protocol方法调用父类注册
|
||||||
|
return super().create_ros_action_server(action_name, action_value_mapping)
|
||||||
|
# 和Base创建的路径是一致的
|
||||||
|
protocol_name = action_name
|
||||||
|
action_type = action_value_mapping["type"]
|
||||||
|
str_action_type = str(action_type)[8:-2]
|
||||||
|
protocol_type = globals()[protocol_name]
|
||||||
|
protocol_steps_generator = action_protocol_generators[protocol_type]
|
||||||
|
|
||||||
|
self._action_servers[action_name] = ActionServer(
|
||||||
|
self,
|
||||||
|
action_type,
|
||||||
|
action_name,
|
||||||
|
execute_callback=self._create_protocol_execute_callback(action_name, protocol_steps_generator),
|
||||||
|
callback_group=ReentrantCallbackGroup(),
|
||||||
|
)
|
||||||
|
self.lab_logger().trace(f"发布动作: {action_name}, 类型: {str_action_type}")
|
||||||
|
return
|
||||||
|
|
||||||
|
def _create_protocol_execute_callback(self, protocol_name, protocol_steps_generator):
|
||||||
|
async def execute_protocol(goal_handle: ServerGoalHandle):
|
||||||
|
"""执行完整的工作流"""
|
||||||
|
# 初始化结果信息变量
|
||||||
|
execution_error = ""
|
||||||
|
execution_success = False
|
||||||
|
protocol_return_value = None
|
||||||
|
self.get_logger().info(f"Executing {protocol_name} action...")
|
||||||
|
action_value_mapping = self._action_value_mappings[protocol_name]
|
||||||
|
step_results = []
|
||||||
|
try:
|
||||||
|
print("+" * 30)
|
||||||
|
print(protocol_steps_generator)
|
||||||
|
# 从目标消息中提取参数, 并调用Protocol生成器(根据设备连接图)生成action步骤
|
||||||
|
goal = goal_handle.request
|
||||||
|
protocol_kwargs = convert_from_ros_msg_with_mapping(goal, action_value_mapping["goal"])
|
||||||
|
|
||||||
|
# # 🔧 添加调试信息
|
||||||
|
# print(f"🔍 转换后的 protocol_kwargs: {protocol_kwargs}")
|
||||||
|
# print(f"🔍 vessel 在转换后: {protocol_kwargs.get('vessel', 'NOT_FOUND')}")
|
||||||
|
|
||||||
|
# # 🔧 完全禁用Host查询,直接使用转换后的数据
|
||||||
|
# print(f"🔧 跳过Host查询,直接使用转换后的数据")
|
||||||
|
# 向Host查询物料当前状态
|
||||||
|
for k, v in goal.get_fields_and_field_types().items():
|
||||||
|
if v in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]:
|
||||||
|
r = ResourceGet.Request()
|
||||||
|
resource_id = (
|
||||||
|
protocol_kwargs[k]["id"] if v == "unilabos_msgs/Resource" else protocol_kwargs[k][0]["id"]
|
||||||
|
)
|
||||||
|
r.id = resource_id
|
||||||
|
r.with_children = True
|
||||||
|
response = await self._resource_clients["resource_get"].call_async(r)
|
||||||
|
protocol_kwargs[k] = list_to_nested_dict(
|
||||||
|
[convert_from_ros_msg(rs) for rs in response.resources]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.lab_logger().info(f"🔍 最终的 vessel: {protocol_kwargs.get('vessel', 'NOT_FOUND')}")
|
||||||
|
|
||||||
|
from unilabos.resources.graphio import physical_setup_graph
|
||||||
|
|
||||||
|
self.lab_logger().info(f"Working on physical setup: {physical_setup_graph}")
|
||||||
|
protocol_steps = protocol_steps_generator(G=physical_setup_graph, **protocol_kwargs)
|
||||||
|
logs = []
|
||||||
|
for step in protocol_steps:
|
||||||
|
if isinstance(step, dict) and "log_message" in step.get("action_kwargs", {}):
|
||||||
|
logs.append(step)
|
||||||
|
elif isinstance(step, list):
|
||||||
|
logs.append(step)
|
||||||
|
self.lab_logger().info(f"Goal received: {protocol_kwargs}, running steps: "
|
||||||
|
f"{json.dumps(logs, indent=4, ensure_ascii=False)}")
|
||||||
|
|
||||||
|
time_start = time.time()
|
||||||
|
time_overall = 100
|
||||||
|
self._busy = True
|
||||||
|
|
||||||
|
# 逐步执行工作流
|
||||||
|
for i, action in enumerate(protocol_steps):
|
||||||
|
# self.get_logger().info(f"Running step {i + 1}: {action}")
|
||||||
|
if isinstance(action, dict):
|
||||||
|
# 如果是单个动作,直接执行
|
||||||
|
if action["action_name"] == "wait":
|
||||||
|
time.sleep(action["action_kwargs"]["time"])
|
||||||
|
step_results.append({"step": i + 1, "action": "wait", "result": "completed"})
|
||||||
|
else:
|
||||||
|
result = await self.execute_single_action(**action)
|
||||||
|
step_results.append({"step": i + 1, "action": action["action_name"], "result": result})
|
||||||
|
ret_info = json.loads(getattr(result, "return_info", "{}"))
|
||||||
|
if not ret_info.get("suc", False):
|
||||||
|
raise RuntimeError(f"Step {i + 1} failed.")
|
||||||
|
elif isinstance(action, list):
|
||||||
|
# 如果是并行动作,同时执行
|
||||||
|
actions = action
|
||||||
|
futures = [
|
||||||
|
rclpy.get_global_executor().create_task(self.execute_single_action(**a)) for a in actions
|
||||||
|
]
|
||||||
|
results = [await f for f in futures]
|
||||||
|
step_results.append(
|
||||||
|
{
|
||||||
|
"step": i + 1,
|
||||||
|
"parallel_actions": [a["action_name"] for a in actions],
|
||||||
|
"results": results,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 向Host更新物料当前状态
|
||||||
|
for k, v in goal.get_fields_and_field_types().items():
|
||||||
|
if v in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]:
|
||||||
|
r = ResourceUpdate.Request()
|
||||||
|
r.resources = [
|
||||||
|
convert_to_ros_msg(Resource, rs) for rs in nested_dict_to_list(protocol_kwargs[k])
|
||||||
|
]
|
||||||
|
response = await self._resource_clients["resource_update"].call_async(r)
|
||||||
|
|
||||||
|
# 设置成功状态和返回值
|
||||||
|
execution_success = True
|
||||||
|
protocol_return_value = {
|
||||||
|
"protocol_name": protocol_name,
|
||||||
|
"steps_executed": len(protocol_steps),
|
||||||
|
"step_results": step_results,
|
||||||
|
"total_time": time.time() - time_start,
|
||||||
}
|
}
|
||||||
|
|
||||||
def transfer_bottle(self, tip_rack: PLRResource, base_plate: PLRResource): # 使用自定义物料的举例
|
goal_handle.succeed()
|
||||||
"""
|
|
||||||
将tip_rack assign给base_plate,两个入参都得是PLRResource,unilabos会代替当前物料操作,自动刷新他们的父子关系等状态
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def trigger_resource_update(self, from_plate: PLRResource, to_base_plate: PLRResource):
|
except Exception as e:
|
||||||
"""
|
# 捕获并记录错误信息
|
||||||
有些时候物料发生了子设备的迁移,一般对该设备的最大一级的物料进行操作,例如要将A物料搬移到B物料上,他们不共同拥有一个物料
|
str_step_results = [
|
||||||
该步骤操作结束后,会主动刷新from_plate的父物料,与to_base_plate的父物料(如没有则刷新自身)
|
{k: dict(message_to_ordereddict(v)) if k == "result" and hasattr(v, "SLOT_TYPES") else v for k, v in
|
||||||
|
i.items()} for i in step_results]
|
||||||
|
execution_error = f"{traceback.format_exc()}\n\nStep Result: {pformat(str_step_results)}"
|
||||||
|
execution_success = False
|
||||||
|
self.lab_logger().error(f"协议 {protocol_name} 执行出错: {str(e)} \n{traceback.format_exc()}")
|
||||||
|
|
||||||
"""
|
# 设置动作失败
|
||||||
to_base_plate.assign_child_resource(from_plate, Coordinate.zero())
|
goal_handle.abort()
|
||||||
pass
|
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self._busy = False
|
||||||
|
|
||||||
|
# 创建结果消息
|
||||||
|
result = action_value_mapping["type"].Result()
|
||||||
|
result.success = execution_success
|
||||||
|
|
||||||
|
# 获取结果消息类型信息,检查是否有return_info字段
|
||||||
|
result_msg_types = action_value_mapping["type"].Result.get_fields_and_field_types()
|
||||||
|
|
||||||
|
# 设置return_info字段(如果存在)
|
||||||
|
for attr_name in result_msg_types.keys():
|
||||||
|
if attr_name in ["success", "reached_goal"]:
|
||||||
|
setattr(result, attr_name, execution_success)
|
||||||
|
elif attr_name == "return_info":
|
||||||
|
setattr(
|
||||||
|
result,
|
||||||
|
attr_name,
|
||||||
|
serialize_result_info(execution_error, execution_success, protocol_return_value),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.lab_logger().info(f"协议 {protocol_name} 完成并返回结果")
|
||||||
|
return result
|
||||||
|
|
||||||
|
return execute_protocol
|
||||||
|
|
||||||
|
async def execute_single_action(self, device_id, action_name, action_kwargs):
|
||||||
|
"""执行单个动作"""
|
||||||
|
# 构建动作ID
|
||||||
|
if device_id in ["", None, "self"]:
|
||||||
|
action_id = f"/devices/{self.device_id}/{action_name}"
|
||||||
|
else:
|
||||||
|
action_id = f"/devices/{device_id}/{action_name}" # 执行时取消了主节点信息 /{self.device_id}
|
||||||
|
|
||||||
|
# 检查动作客户端是否存在
|
||||||
|
if action_id not in self._action_clients:
|
||||||
|
self.lab_logger().error(f"找不到动作客户端: {action_id}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 发送动作请求
|
||||||
|
action_client = self._action_clients[action_id]
|
||||||
|
goal_msg = convert_to_ros_msg(action_client._action_type.Goal(), action_kwargs)
|
||||||
|
|
||||||
|
##### self.lab_logger().info(f"发送动作请求到: {action_id}")
|
||||||
|
action_client.wait_for_server()
|
||||||
|
|
||||||
|
# 等待动作完成
|
||||||
|
request_future = action_client.send_goal_async(goal_msg)
|
||||||
|
handle = await request_future
|
||||||
|
|
||||||
|
if not handle.accepted:
|
||||||
|
self.lab_logger().error(f"动作请求被拒绝: {action_name}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
result_future = await handle.get_result_async()
|
||||||
|
##### self.lab_logger().info(f"动作完成: {action_name}")
|
||||||
|
|
||||||
|
return result_future.result
|
||||||
|
|
||||||
|
"""还没有改过的部分"""
|
||||||
|
|
||||||
|
def _setup_hardware_proxy(
|
||||||
|
self, device: ROS2DeviceNode, communication_device: ROS2DeviceNode, read_method, write_method
|
||||||
|
):
|
||||||
|
"""为设备设置硬件接口代理"""
|
||||||
|
# extra_info = [getattr(device.driver_instance, info) for info in communication_device.ros_node_instance._hardware_interface.get("extra_info", [])]
|
||||||
|
write_func = getattr(
|
||||||
|
communication_device.driver_instance, communication_device.ros_node_instance._hardware_interface["write"]
|
||||||
|
)
|
||||||
|
read_func = getattr(
|
||||||
|
communication_device.driver_instance, communication_device.ros_node_instance._hardware_interface["read"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def _read(*args, **kwargs):
|
||||||
|
return read_func(*args, **kwargs)
|
||||||
|
|
||||||
|
def _write(*args, **kwargs):
|
||||||
|
return write_func(*args, **kwargs)
|
||||||
|
|
||||||
|
if read_method:
|
||||||
|
# bound_read = MethodType(_read, device.driver_instance)
|
||||||
|
setattr(device.driver_instance, read_method, _read)
|
||||||
|
|
||||||
|
if write_method:
|
||||||
|
# bound_write = MethodType(_write, device.driver_instance)
|
||||||
|
setattr(device.driver_instance, write_method, _write)
|
||||||
|
|
||||||
|
async def _update_resources(self, goal, protocol_kwargs):
|
||||||
|
"""更新资源状态"""
|
||||||
|
for k, v in goal.get_fields_and_field_types().items():
|
||||||
|
if v in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]:
|
||||||
|
if protocol_kwargs[k] is not None:
|
||||||
|
try:
|
||||||
|
r = ResourceUpdate.Request()
|
||||||
|
r.resources = [
|
||||||
|
convert_to_ros_msg(Resource, rs) for rs in nested_dict_to_list(protocol_kwargs[k])
|
||||||
|
]
|
||||||
|
await self._resource_clients["resource_update"].call_async(r)
|
||||||
|
except Exception as e:
|
||||||
|
self.lab_logger().error(f"更新资源失败: {e}")
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
from typing import List, Tuple, Any
|
from typing import List, Tuple, Any, Dict, TYPE_CHECKING
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
from unilabos.utils.log import logger
|
from unilabos.utils.log import logger
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from unilabos.devices.workstation.workstation_base import WorkstationBase
|
||||||
|
from pylabrobot.resources import Resource as PLRResource
|
||||||
|
|
||||||
|
|
||||||
class DeviceNodeResourceTracker(object):
|
class DeviceNodeResourceTracker(object):
|
||||||
|
|
||||||
@@ -37,10 +42,20 @@ class DeviceNodeResourceTracker(object):
|
|||||||
def figure_resource(self, query_resource, try_mode=False):
|
def figure_resource(self, query_resource, try_mode=False):
|
||||||
if isinstance(query_resource, list):
|
if isinstance(query_resource, list):
|
||||||
return [self.figure_resource(r, try_mode) for r in query_resource]
|
return [self.figure_resource(r, try_mode) for r in query_resource]
|
||||||
elif isinstance(query_resource, dict) and "id" not in query_resource and "name" not in query_resource: # 临时处理,要删除的,driver有太多类型错误标注
|
elif (
|
||||||
|
isinstance(query_resource, dict) and "id" not in query_resource and "name" not in query_resource
|
||||||
|
): # 临时处理,要删除的,driver有太多类型错误标注
|
||||||
return [self.figure_resource(r, try_mode) for r in query_resource.values()]
|
return [self.figure_resource(r, try_mode) for r in query_resource.values()]
|
||||||
res_id = query_resource.id if hasattr(query_resource, "id") else (query_resource.get("id") if isinstance(query_resource, dict) else None)
|
res_id = (
|
||||||
res_name = query_resource.name if hasattr(query_resource, "name") else (query_resource.get("name") if isinstance(query_resource, dict) else None)
|
query_resource.id
|
||||||
|
if hasattr(query_resource, "id")
|
||||||
|
else (query_resource.get("id") if isinstance(query_resource, dict) else None)
|
||||||
|
)
|
||||||
|
res_name = (
|
||||||
|
query_resource.name
|
||||||
|
if hasattr(query_resource, "name")
|
||||||
|
else (query_resource.get("name") if isinstance(query_resource, dict) else None)
|
||||||
|
)
|
||||||
res_identifier = res_id if res_id else res_name
|
res_identifier = res_id if res_id else res_name
|
||||||
identifier_key = "id" if res_id else "name"
|
identifier_key = "id" if res_id else "name"
|
||||||
resource_cls_type = type(query_resource)
|
resource_cls_type = type(query_resource)
|
||||||
@@ -54,7 +69,9 @@ class DeviceNodeResourceTracker(object):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
res_list.extend(
|
res_list.extend(
|
||||||
self.loop_find_resource(r, resource_cls_type, identifier_key, getattr(query_resource, identifier_key))
|
self.loop_find_resource(
|
||||||
|
r, resource_cls_type, identifier_key, getattr(query_resource, identifier_key)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if not try_mode:
|
if not try_mode:
|
||||||
assert len(res_list) > 0, f"没有找到资源 {query_resource},请检查资源是否存在"
|
assert len(res_list) > 0, f"没有找到资源 {query_resource},请检查资源是否存在"
|
||||||
@@ -66,12 +83,16 @@ class DeviceNodeResourceTracker(object):
|
|||||||
self.resource2parent_resource[id(res_list[0][1])] = res_list[0][0]
|
self.resource2parent_resource[id(res_list[0][1])] = res_list[0][0]
|
||||||
return res_list[0][1]
|
return res_list[0][1]
|
||||||
|
|
||||||
def loop_find_resource(self, resource, target_resource_cls_type, identifier_key, compare_value, parent_res=None) -> List[Tuple[Any, Any]]:
|
def loop_find_resource(
|
||||||
|
self, resource, target_resource_cls_type, identifier_key, compare_value, parent_res=None
|
||||||
|
) -> List[Tuple[Any, Any]]:
|
||||||
res_list = []
|
res_list = []
|
||||||
# print(resource, target_resource_cls_type, identifier_key, compare_value)
|
# print(resource, target_resource_cls_type, identifier_key, compare_value)
|
||||||
children = getattr(resource, "children", [])
|
children = getattr(resource, "children", [])
|
||||||
for child in children:
|
for child in children:
|
||||||
res_list.extend(self.loop_find_resource(child, target_resource_cls_type, identifier_key, compare_value, resource))
|
res_list.extend(
|
||||||
|
self.loop_find_resource(child, target_resource_cls_type, identifier_key, compare_value, resource)
|
||||||
|
)
|
||||||
if target_resource_cls_type == type(resource):
|
if target_resource_cls_type == type(resource):
|
||||||
if target_resource_cls_type == dict:
|
if target_resource_cls_type == dict:
|
||||||
if identifier_key in resource:
|
if identifier_key in resource:
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import inspect
|
import inspect
|
||||||
import json
|
|
||||||
import traceback
|
import traceback
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from typing import Type, Any, Dict, Optional, TypeVar, Generic
|
from typing import Type, Any, Dict, Optional, TypeVar, Generic
|
||||||
@@ -267,40 +266,45 @@ class PyLabRobotCreator(DeviceClassCreator[T]):
|
|||||||
ROS2DeviceNode.run_async_func(getattr(self.device_instance, "setup")).add_done_callback(done_cb)
|
ROS2DeviceNode.run_async_func(getattr(self.device_instance, "setup")).add_done_callback(done_cb)
|
||||||
|
|
||||||
|
|
||||||
class ProtocolNodeCreator(DeviceClassCreator[T]):
|
class WorkstationNodeCreator(DeviceClassCreator[T]):
|
||||||
"""
|
"""
|
||||||
ProtocolNode设备类创建器
|
WorkstationNode设备类创建器
|
||||||
|
|
||||||
这个类提供了针对ProtocolNode设备类的实例创建方法,处理children参数。
|
这个类提供了针对WorkstationNode设备类的实例创建方法,处理children参数。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, cls: Type[T], children: Dict[str, Any], resource_tracker: DeviceNodeResourceTracker):
|
def __init__(self, cls: Type[T], children: Dict[str, Any], resource_tracker: DeviceNodeResourceTracker):
|
||||||
"""
|
"""
|
||||||
初始化ProtocolNode设备类创建器
|
初始化WorkstationNode设备类创建器
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
cls: ProtocolNode设备类
|
cls: WorkstationNode设备类
|
||||||
children: 子资源字典,用于资源替换
|
children: 子资源字典,用于资源替换
|
||||||
"""
|
"""
|
||||||
super().__init__(cls, children, resource_tracker)
|
super().__init__(cls, children, resource_tracker)
|
||||||
|
|
||||||
def create_instance(self, data: Dict[str, Any]) -> T:
|
def create_instance(self, data: Dict[str, Any]) -> T:
|
||||||
"""
|
"""
|
||||||
从数据创建ProtocolNode设备实例
|
从数据创建WorkstationNode设备实例
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
data: 用于创建实例的数据
|
data: 用于创建实例的数据
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
ProtocolNode设备类实例
|
WorkstationNode设备类实例
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 创建实例,额外补充一个给protocol node的字段,后面考虑取消
|
# 创建实例,额外补充一个给protocol node的字段,后面考虑取消
|
||||||
data["children"] = self.children
|
data["children"] = self.children
|
||||||
self.device_instance = super(ProtocolNodeCreator, self).create_instance(data)
|
station_resource_dict = data["station_resource"]
|
||||||
|
from pylabrobot.resources import Deck, Resource
|
||||||
|
plrc = PyLabRobotCreator(Deck, self.children, self.resource_tracker)
|
||||||
|
station_resource = plrc.create_instance(station_resource_dict)
|
||||||
|
data["station_resource"] = station_resource
|
||||||
|
self.device_instance = super(WorkstationNodeCreator, self).create_instance(data)
|
||||||
self.post_create()
|
self.post_create()
|
||||||
return self.device_instance
|
return self.device_instance
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"ProtocolNode创建实例失败: {e}")
|
logger.error(f"WorkstationNode创建实例失败: {e}")
|
||||||
logger.error(f"ProtocolNode创建实例堆栈: {traceback.format_exc()}")
|
logger.error(f"WorkstationNode创建实例堆栈: {traceback.format_exc()}")
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -98,6 +98,19 @@ set(action_files
|
|||||||
|
|
||||||
"action/WorkStationRun.action"
|
"action/WorkStationRun.action"
|
||||||
"action/AGVTransfer.action"
|
"action/AGVTransfer.action"
|
||||||
|
|
||||||
|
"action/DispenStationSolnPrep.action"
|
||||||
|
"action/DispenStationVialFeed.action"
|
||||||
|
|
||||||
|
"action/PostProcessGrab.action"
|
||||||
|
"action/PostProcessTriggerClean.action"
|
||||||
|
"action/PostProcessTriggerPostPro.action"
|
||||||
|
|
||||||
|
"action/ReactionStationDripBack.action"
|
||||||
|
"action/ReactionStationLiquidFeed.action"
|
||||||
|
"action/ReactionStationProExecu.action"
|
||||||
|
"action/ReactionStationReaTackIn.action"
|
||||||
|
"action/ReactionStationSolidFeedVial.action"
|
||||||
)
|
)
|
||||||
|
|
||||||
set(srv_files
|
set(srv_files
|
||||||
|
|||||||
15
unilabos_msgs/action/DispenStationSolnPrep.action
Normal file
15
unilabos_msgs/action/DispenStationSolnPrep.action
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Goal - (二胺)溶液配置
|
||||||
|
string order_name # 任务名称
|
||||||
|
string material_name #固体物料名称
|
||||||
|
string target_weigh #固体重量
|
||||||
|
string volume #液体体积
|
||||||
|
string liquid_material_name # 溶剂名称
|
||||||
|
string speed #磁力转动速度
|
||||||
|
string temperature #温度
|
||||||
|
string delay_time #等待时间
|
||||||
|
string hold_m_name #样品名称
|
||||||
|
---
|
||||||
|
# Result - 操作结果
|
||||||
|
string return_info # 结果消息
|
||||||
|
---
|
||||||
|
# Feedback - 实时反馈
|
||||||
29
unilabos_msgs/action/DispenStationVialFeed.action
Normal file
29
unilabos_msgs/action/DispenStationVialFeed.action
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Goal - 小瓶投料
|
||||||
|
string order_name # 任务名称
|
||||||
|
string percent_90_1_assign_material_name
|
||||||
|
string percent_90_1_target_weigh
|
||||||
|
string percent_90_2_assign_material_name
|
||||||
|
string percent_90_2_target_weigh
|
||||||
|
string percent_90_3_assign_material_name
|
||||||
|
string percent_90_3_target_weigh
|
||||||
|
string percent_10_1_assign_material_name
|
||||||
|
string percent_10_1_target_weigh
|
||||||
|
string percent_10_1_volume
|
||||||
|
string percent_10_1_liquid_material_name
|
||||||
|
string percent_10_2_assign_material_name
|
||||||
|
string percent_10_2_target_weigh
|
||||||
|
string percent_10_2_volume
|
||||||
|
string percent_10_2_liquid_material_name
|
||||||
|
string percent_10_3_assign_material_name
|
||||||
|
string percent_10_3_target_weigh
|
||||||
|
string percent_10_3_volume
|
||||||
|
string percent_10_3_liquid_material_name
|
||||||
|
string speed
|
||||||
|
string temperature
|
||||||
|
string delay_time
|
||||||
|
string hold_m_name
|
||||||
|
---
|
||||||
|
# Result - 操作结果
|
||||||
|
string return_info # 结果消息
|
||||||
|
---
|
||||||
|
# Feedback - 实时反馈
|
||||||
8
unilabos_msgs/action/PostProcessGrab.action
Normal file
8
unilabos_msgs/action/PostProcessGrab.action
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Goal - 抓取参数
|
||||||
|
int32 reaction_tank_number #反应罐号码
|
||||||
|
int32 raw_tank_number #原料罐号码
|
||||||
|
---
|
||||||
|
# Result - 操作结果
|
||||||
|
string return_info
|
||||||
|
---
|
||||||
|
# Feedback - 实时反馈
|
||||||
46
unilabos_msgs/action/PostProcessTriggerClean.action
Normal file
46
unilabos_msgs/action/PostProcessTriggerClean.action
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Goal - 清洗参数
|
||||||
|
float64 nmp_outer_wall_cleaning_injection
|
||||||
|
int32 nmp_outer_wall_cleaning_count
|
||||||
|
int32 nmp_outer_wall_cleaning_wait_time
|
||||||
|
int32 nmp_outer_wall_cleaning_waste_time
|
||||||
|
float64 nmp_inner_wall_cleaning_injection
|
||||||
|
int32 nmp_inner_wall_cleaning_count
|
||||||
|
int32 nmp_pump_cleaning_suction_count
|
||||||
|
int32 nmp_inner_wall_cleaning_waste_time
|
||||||
|
float64 nmp_stirrer_cleaning_injection
|
||||||
|
int32 nmp_stirrer_cleaning_count
|
||||||
|
int32 nmp_stirrer_cleaning_wait_time
|
||||||
|
int32 nmp_stirrer_cleaning_waste_time
|
||||||
|
float64 water_outer_wall_cleaning_injection
|
||||||
|
int32 water_outer_wall_cleaning_count
|
||||||
|
int32 water_outer_wall_cleaning_wait_time
|
||||||
|
int32 water_outer_wall_cleaning_waste_time
|
||||||
|
float64 water_inner_wall_cleaning_injection
|
||||||
|
int32 water_inner_wall_cleaning_count
|
||||||
|
int32 water_pump_cleaning_suction_count
|
||||||
|
int32 water_inner_wall_cleaning_waste_time
|
||||||
|
float64 water_stirrer_cleaning_injection
|
||||||
|
int32 water_stirrer_cleaning_count
|
||||||
|
int32 water_stirrer_cleaning_wait_time
|
||||||
|
int32 water_stirrer_cleaning_waste_time
|
||||||
|
float64 acetone_outer_wall_cleaning_injection
|
||||||
|
int32 acetone_outer_wall_cleaning_count
|
||||||
|
int32 acetone_outer_wall_cleaning_wait_time
|
||||||
|
int32 acetone_outer_wall_cleaning_waste_time
|
||||||
|
float64 acetone_inner_wall_cleaning_injection
|
||||||
|
int32 acetone_inner_wall_cleaning_count
|
||||||
|
int32 acetone_pump_cleaning_suction_count
|
||||||
|
int32 acetone_inner_wall_cleaning_waste_time
|
||||||
|
float64 acetone_stirrer_cleaning_injection
|
||||||
|
int32 acetone_stirrer_cleaning_count
|
||||||
|
int32 acetone_stirrer_cleaning_wait_time
|
||||||
|
int32 acetone_stirrer_cleaning_waste_time
|
||||||
|
int32 pipe_blowing_time
|
||||||
|
int32 injection_pump_forward_empty_suction_count
|
||||||
|
int32 injection_pump_reverse_empty_suction_count
|
||||||
|
int32 filtration_liquid_selection
|
||||||
|
---
|
||||||
|
# Result - 操作结果
|
||||||
|
string return_info # 操作是否成功
|
||||||
|
---
|
||||||
|
# Feedback - 实时反馈
|
||||||
20
unilabos_msgs/action/PostProcessTriggerPostPro.action
Normal file
20
unilabos_msgs/action/PostProcessTriggerPostPro.action
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Goal - 后处理参数
|
||||||
|
float64 atomization_fast_speed
|
||||||
|
float64 wash_slow_speed
|
||||||
|
int32 injection_pump_suction_speed
|
||||||
|
int32 injection_pump_push_speed
|
||||||
|
int32 raw_liquid_suction_count
|
||||||
|
float64 first_wash_water_amount
|
||||||
|
float64 second_wash_water_amount
|
||||||
|
int32 first_powder_mixing_tim
|
||||||
|
int32 second_powder_mixing_time
|
||||||
|
int32 first_powder_wash_count
|
||||||
|
int32 second_powder_wash_count
|
||||||
|
float64 initial_water_amount
|
||||||
|
int32 pre_filtration_mixing_time
|
||||||
|
int32 atomization_pressure_kpa
|
||||||
|
---
|
||||||
|
# Result - 操作结果
|
||||||
|
string return_info # 操作是否成功
|
||||||
|
---
|
||||||
|
# Feedback - 实时反馈
|
||||||
11
unilabos_msgs/action/ReactionStationDripBack.action
Normal file
11
unilabos_msgs/action/ReactionStationDripBack.action
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Goal - 滴回去
|
||||||
|
string volume # 投料体积
|
||||||
|
string assign_material_name # 溶剂名称
|
||||||
|
string time # 观察时间(单位min)
|
||||||
|
string torque_variation #是否观察1否2是
|
||||||
|
---
|
||||||
|
# Result - 操作结果
|
||||||
|
string return_info # 结果消息
|
||||||
|
|
||||||
|
---
|
||||||
|
# Feedback - 实时反馈
|
||||||
11
unilabos_msgs/action/ReactionStationLiquidFeed.action
Normal file
11
unilabos_msgs/action/ReactionStationLiquidFeed.action
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Goal - 液体投料
|
||||||
|
string titration_type # 滴定类型1否2是
|
||||||
|
string volume # 投料体积
|
||||||
|
string assign_material_name # 溶剂名称
|
||||||
|
string time # 观察时间(单位min)
|
||||||
|
string torque_variation #是否观察1否2是
|
||||||
|
---
|
||||||
|
# Result - 操作结果
|
||||||
|
string return_info # 结果消息
|
||||||
|
---
|
||||||
|
# Feedback - 实时反馈
|
||||||
8
unilabos_msgs/action/ReactionStationProExecu.action
Normal file
8
unilabos_msgs/action/ReactionStationProExecu.action
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Goal - 合并工作流+执行
|
||||||
|
string workflow_name # 工作流名称
|
||||||
|
string task_name # 任务名称
|
||||||
|
---
|
||||||
|
# Result - 操作结果
|
||||||
|
string return_info # 结果消息
|
||||||
|
---
|
||||||
|
# Feedback - 实时反馈
|
||||||
9
unilabos_msgs/action/ReactionStationReaTackIn.action
Normal file
9
unilabos_msgs/action/ReactionStationReaTackIn.action
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Goal - 通量-配置
|
||||||
|
string cutoff # 黏度_通量-配置
|
||||||
|
string temperature # 温度_通量-配置
|
||||||
|
string assign_material_name # 分液类型_通量-配置
|
||||||
|
---
|
||||||
|
# Result - 操作结果
|
||||||
|
string return_info # 结果消息
|
||||||
|
---
|
||||||
|
# Feedback - 实时反馈
|
||||||
10
unilabos_msgs/action/ReactionStationSolidFeedVial.action
Normal file
10
unilabos_msgs/action/ReactionStationSolidFeedVial.action
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Goal - 固体投料-小瓶
|
||||||
|
string assign_material_name # 固体名称_粉末加样模块-投料
|
||||||
|
string material_id # 固体投料类型_粉末加样模块-投料
|
||||||
|
string time # 观察时间_反应模块-观察搅拌结果
|
||||||
|
string torque_variation #是否观察1否2是_反应模块-观察搅拌结果
|
||||||
|
---
|
||||||
|
# Result - 操作结果
|
||||||
|
string return_info # 结果消息
|
||||||
|
---
|
||||||
|
# Feedback - 实时反馈
|
||||||
Reference in New Issue
Block a user