mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2026-02-11 18:25:10 +00:00
Compare commits
63 Commits
v0.10.17
...
33c30bc92e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33c30bc92e | ||
|
|
2a8ed542de | ||
|
|
361eae2f6d | ||
|
|
c25283ae04 | ||
|
|
961752fb0d | ||
|
|
53b1fde731 | ||
|
|
55165024dd | ||
|
|
6ddceb8393 | ||
|
|
4e52c7d2f4 | ||
|
|
0b56efc89d | ||
|
|
5108678cf5 | ||
|
|
a27b93396a | ||
|
|
7b004f43e8 | ||
|
|
2a60a6c27e | ||
|
|
5dda94044d | ||
|
|
0cfc6f45e3 | ||
|
|
831f4549f9 | ||
|
|
f4d4eb06d3 | ||
|
|
e3b8164f6b | ||
|
|
78c04acc2e | ||
|
|
cd0428ea78 | ||
|
|
bdddbd57ba | ||
|
|
a312de08a5 | ||
|
|
68513b5745 | ||
|
|
19027350fb | ||
|
|
bbbdb06bbc | ||
|
|
cd84e26126 | ||
|
|
ce5bab3af1 | ||
|
|
82d9ef6bf7 | ||
|
|
332b33c6f4 | ||
|
|
1ec642ee3a | ||
|
|
7d8e6d029b | ||
|
|
5ec8a57a1f | ||
|
|
ae3c1100ae | ||
|
|
14bc2e6cda | ||
|
|
9f823a4198 | ||
|
|
02c79363c1 | ||
|
|
227ff1284a | ||
|
|
4b7bde6be5 | ||
|
|
8a669ac35a | ||
|
|
a1538da39e | ||
|
|
0063df4cf3 | ||
|
|
e570ba4976 | ||
|
|
e8c1f76dbb | ||
|
|
f791c1a342 | ||
|
|
ea60cbe891 | ||
|
|
eac9b8ab3d | ||
|
|
573bcf1a6c | ||
|
|
50e93cb1af | ||
|
|
fe1a029a9b | ||
|
|
662c063f50 | ||
|
|
01cbbba0b3 | ||
|
|
e6c556cf19 | ||
|
|
0605f305ed | ||
|
|
37d8108ec4 | ||
|
|
6081dac561 | ||
|
|
5b2d066127 | ||
|
|
06e66765e7 | ||
|
|
98ce360088 | ||
|
|
5cd0f72fbd | ||
|
|
343f394203 | ||
|
|
46aa7a7bd2 | ||
|
|
a66369e2c3 |
@@ -1,6 +1,6 @@
|
|||||||
package:
|
package:
|
||||||
name: unilabos
|
name: unilabos
|
||||||
version: 0.10.3
|
version: 0.10.4
|
||||||
|
|
||||||
source:
|
source:
|
||||||
path: ../unilabos
|
path: ../unilabos
|
||||||
@@ -36,6 +36,7 @@ requirements:
|
|||||||
- conda-forge::python ==3.11.11
|
- conda-forge::python ==3.11.11
|
||||||
- compilers
|
- compilers
|
||||||
- cmake
|
- cmake
|
||||||
|
- zstd
|
||||||
- ninja
|
- ninja
|
||||||
- if: unix
|
- if: unix
|
||||||
then:
|
then:
|
||||||
@@ -60,7 +61,7 @@ requirements:
|
|||||||
- uvicorn
|
- uvicorn
|
||||||
- gradio
|
- gradio
|
||||||
- flask
|
- flask
|
||||||
- websocket
|
- websockets
|
||||||
- ipython
|
- ipython
|
||||||
- jupyter
|
- jupyter
|
||||||
- jupyros
|
- jupyros
|
||||||
@@ -85,5 +86,5 @@ requirements:
|
|||||||
|
|
||||||
about:
|
about:
|
||||||
repository: https://github.com/dptech-corp/Uni-Lab-OS
|
repository: https://github.com/dptech-corp/Uni-Lab-OS
|
||||||
license: GPL-3.0
|
license: GPL-3.0-only
|
||||||
description: "Uni-Lab-OS"
|
description: "Uni-Lab-OS"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
# Uni-Lab-OS
|
# Uni-Lab-OS
|
||||||
|
|
||||||
<!-- Language switcher -->
|
<!-- Language switcher -->
|
||||||
|
|
||||||
**English** | [中文](README_zh.md)
|
**English** | [中文](README_zh.md)
|
||||||
|
|
||||||
[](https://github.com/dptech-corp/Uni-Lab-OS/stargazers)
|
[](https://github.com/dptech-corp/Uni-Lab-OS/stargazers)
|
||||||
@@ -74,4 +75,4 @@ This project is licensed under GPL-3.0 - see the [LICENSE](LICENSE) file for det
|
|||||||
|
|
||||||
## Contact Us
|
## Contact Us
|
||||||
|
|
||||||
- GitHub Issues: [https://github.com/dptech-corp/Uni-Lab-OS/issues](https://github.com/dptech-corp/Uni-Lab-OS/issues)
|
- GitHub Issues: [https://github.com/dptech-corp/Uni-Lab-OS/issues](https://github.com/dptech-corp/Uni-Lab-OS/issues)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
# Uni-Lab-OS
|
# Uni-Lab-OS
|
||||||
|
|
||||||
<!-- Language switcher -->
|
<!-- Language switcher -->
|
||||||
|
|
||||||
[English](README.md) | **中文**
|
[English](README.md) | **中文**
|
||||||
|
|
||||||
[](https://github.com/dptech-corp/Uni-Lab-OS/stargazers)
|
[](https://github.com/dptech-corp/Uni-Lab-OS/stargazers)
|
||||||
@@ -12,7 +13,7 @@
|
|||||||
[](https://github.com/dptech-corp/Uni-Lab-OS/issues)
|
[](https://github.com/dptech-corp/Uni-Lab-OS/issues)
|
||||||
[](https://github.com/dptech-corp/Uni-Lab-OS/blob/main/LICENSE)
|
[](https://github.com/dptech-corp/Uni-Lab-OS/blob/main/LICENSE)
|
||||||
|
|
||||||
Uni-Lab-OS是一个用于实验室自动化的综合平台,旨在连接和控制各种实验设备,实现实验流程的自动化和标准化。
|
Uni-Lab-OS 是一个用于实验室自动化的综合平台,旨在连接和控制各种实验设备,实现实验流程的自动化和标准化。
|
||||||
|
|
||||||
## 🏆 比赛
|
## 🏆 比赛
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ Uni-Lab-OS是一个用于实验室自动化的综合平台,旨在连接和控
|
|||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
1. 配置Conda环境
|
1. 配置 Conda 环境
|
||||||
|
|
||||||
Uni-Lab-OS 建议使用 `mamba` 管理环境。根据您的操作系统选择适当的环境文件:
|
Uni-Lab-OS 建议使用 `mamba` 管理环境。根据您的操作系统选择适当的环境文件:
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ Uni-Lab-OS 建议使用 `mamba` 管理环境。根据您的操作系统选择适
|
|||||||
mamba create -n unilab uni-lab::unilabos -c robostack-staging -c conda-forge
|
mamba create -n unilab uni-lab::unilabos -c robostack-staging -c conda-forge
|
||||||
```
|
```
|
||||||
|
|
||||||
2. 安装开发版Uni-Lab-OS:
|
2. 安装开发版 Uni-Lab-OS:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 克隆仓库
|
# 克隆仓库
|
||||||
@@ -76,4 +77,4 @@ Uni-Lab-OS 使用预构建的 `unilabos_msgs` 进行系统通信。您可以在
|
|||||||
|
|
||||||
## 联系我们
|
## 联系我们
|
||||||
|
|
||||||
- GitHub Issues: [https://github.com/dptech-corp/Uni-Lab-OS/issues](https://github.com/dptech-corp/Uni-Lab-OS/issues)
|
- GitHub Issues: [https://github.com/dptech-corp/Uni-Lab-OS/issues](https://github.com/dptech-corp/Uni-Lab-OS/issues)
|
||||||
|
|||||||
@@ -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. **强大的错误处理**: 多层次的错误处理和恢复机制
|
||||||
588
example_devices.py
Normal file
588
example_devices.py
Normal file
@@ -0,0 +1,588 @@
|
|||||||
|
"""
|
||||||
|
示例设备类文件,用于测试注册表编辑器
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from typing import Dict, Any, Optional, List
|
||||||
|
|
||||||
|
|
||||||
|
class SmartPumpController:
|
||||||
|
"""
|
||||||
|
智能泵控制器
|
||||||
|
|
||||||
|
支持多种泵送模式,具有高精度流量控制和自动校准功能。
|
||||||
|
适用于实验室自动化系统中的液体处理任务。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, device_id: str = "smart_pump_01", port: str = "/dev/ttyUSB0"):
|
||||||
|
"""
|
||||||
|
初始化智能泵控制器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
device_id: 设备唯一标识符
|
||||||
|
port: 通信端口
|
||||||
|
"""
|
||||||
|
self.device_id = device_id
|
||||||
|
self.port = port
|
||||||
|
self.is_connected = False
|
||||||
|
self.current_flow_rate = 0.0
|
||||||
|
self.total_volume_pumped = 0.0
|
||||||
|
self.calibration_factor = 1.0
|
||||||
|
self.pump_mode = "continuous" # continuous, volume, rate
|
||||||
|
|
||||||
|
def connect_device(self, timeout: int = 10) -> bool:
|
||||||
|
"""
|
||||||
|
连接到泵设备
|
||||||
|
|
||||||
|
Args:
|
||||||
|
timeout: 连接超时时间(秒)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 连接是否成功
|
||||||
|
"""
|
||||||
|
# 模拟连接过程
|
||||||
|
self.is_connected = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
def disconnect_device(self) -> bool:
|
||||||
|
"""
|
||||||
|
断开设备连接
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 断开连接是否成功
|
||||||
|
"""
|
||||||
|
self.is_connected = False
|
||||||
|
self.current_flow_rate = 0.0
|
||||||
|
return True
|
||||||
|
|
||||||
|
def set_flow_rate(self, flow_rate: float, units: str = "ml/min") -> bool:
|
||||||
|
"""
|
||||||
|
设置泵流速
|
||||||
|
|
||||||
|
Args:
|
||||||
|
flow_rate: 流速值
|
||||||
|
units: 流速单位
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 设置是否成功
|
||||||
|
"""
|
||||||
|
if not self.is_connected:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.current_flow_rate = flow_rate
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def pump_volume_async(self, volume: float, flow_rate: float) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
异步泵送指定体积的液体
|
||||||
|
|
||||||
|
Args:
|
||||||
|
volume: 目标体积 (mL)
|
||||||
|
flow_rate: 泵送流速 (mL/min)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict: 包含操作结果的字典
|
||||||
|
"""
|
||||||
|
if not self.is_connected:
|
||||||
|
return {"success": False, "error": "设备未连接"}
|
||||||
|
|
||||||
|
# 计算泵送时间
|
||||||
|
pump_time = (volume / flow_rate) * 60 # 转换为秒
|
||||||
|
|
||||||
|
self.current_flow_rate = flow_rate
|
||||||
|
await asyncio.sleep(min(pump_time, 3.0)) # 模拟泵送过程
|
||||||
|
|
||||||
|
self.total_volume_pumped += volume
|
||||||
|
self.current_flow_rate = 0.0
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"pumped_volume": volume,
|
||||||
|
"actual_time": min(pump_time, 3.0),
|
||||||
|
"total_volume": self.total_volume_pumped,
|
||||||
|
}
|
||||||
|
|
||||||
|
def emergency_stop(self) -> bool:
|
||||||
|
"""
|
||||||
|
紧急停止泵
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 停止是否成功
|
||||||
|
"""
|
||||||
|
self.current_flow_rate = 0.0
|
||||||
|
return True
|
||||||
|
|
||||||
|
def perform_calibration(self, reference_volume: float, measured_volume: float) -> bool:
|
||||||
|
"""
|
||||||
|
执行泵校准
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reference_volume: 参考体积
|
||||||
|
measured_volume: 实际测量体积
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 校准是否成功
|
||||||
|
"""
|
||||||
|
if measured_volume > 0:
|
||||||
|
self.calibration_factor = reference_volume / measured_volume
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 状态查询方法
|
||||||
|
def get_connection_status(self) -> str:
|
||||||
|
"""获取连接状态"""
|
||||||
|
return "connected" if self.is_connected else "disconnected"
|
||||||
|
|
||||||
|
def get_current_flow_rate(self) -> float:
|
||||||
|
"""获取当前流速 (mL/min)"""
|
||||||
|
return self.current_flow_rate
|
||||||
|
|
||||||
|
def get_total_volume(self) -> float:
|
||||||
|
"""获取累计泵送体积 (mL)"""
|
||||||
|
return self.total_volume_pumped
|
||||||
|
|
||||||
|
def get_calibration_factor(self) -> float:
|
||||||
|
"""获取校准因子"""
|
||||||
|
return self.calibration_factor
|
||||||
|
|
||||||
|
def get_pump_mode(self) -> str:
|
||||||
|
"""获取泵送模式"""
|
||||||
|
return self.pump_mode
|
||||||
|
|
||||||
|
def get_device_status(self) -> Dict[str, Any]:
|
||||||
|
"""获取设备完整状态信息"""
|
||||||
|
return {
|
||||||
|
"device_id": self.device_id,
|
||||||
|
"connected": self.is_connected,
|
||||||
|
"flow_rate": self.current_flow_rate,
|
||||||
|
"total_volume": self.total_volume_pumped,
|
||||||
|
"calibration_factor": self.calibration_factor,
|
||||||
|
"mode": self.pump_mode,
|
||||||
|
"running": self.current_flow_rate > 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AdvancedTemperatureController:
|
||||||
|
"""
|
||||||
|
高级温度控制器
|
||||||
|
|
||||||
|
支持PID控制、多点温度监控和程序化温度曲线。
|
||||||
|
适用于需要精确温度控制的化学反应和材料处理过程。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, controller_id: str = "temp_controller_01"):
|
||||||
|
"""
|
||||||
|
初始化温度控制器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
controller_id: 控制器ID
|
||||||
|
"""
|
||||||
|
self.controller_id = controller_id
|
||||||
|
self.current_temperature = 25.0
|
||||||
|
self.target_temperature = 25.0
|
||||||
|
self.is_heating = False
|
||||||
|
self.is_cooling = False
|
||||||
|
self.pid_enabled = True
|
||||||
|
self.temperature_history: List[Dict] = []
|
||||||
|
|
||||||
|
def set_target_temperature(self, temperature: float, rate: float = 10.0) -> bool:
|
||||||
|
"""
|
||||||
|
设置目标温度
|
||||||
|
|
||||||
|
Args:
|
||||||
|
temperature: 目标温度 (°C)
|
||||||
|
rate: 升温/降温速率 (°C/min)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 设置是否成功
|
||||||
|
"""
|
||||||
|
self.target_temperature = temperature
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def heat_to_temperature_async(
|
||||||
|
self, temperature: float, tolerance: float = 0.5, timeout: int = 600
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
异步加热到指定温度
|
||||||
|
|
||||||
|
Args:
|
||||||
|
temperature: 目标温度 (°C)
|
||||||
|
tolerance: 温度容差 (°C)
|
||||||
|
timeout: 最大等待时间 (秒)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict: 操作结果
|
||||||
|
"""
|
||||||
|
self.target_temperature = temperature
|
||||||
|
start_temp = self.current_temperature
|
||||||
|
|
||||||
|
if temperature > start_temp:
|
||||||
|
self.is_heating = True
|
||||||
|
elif temperature < start_temp:
|
||||||
|
self.is_cooling = True
|
||||||
|
|
||||||
|
# 模拟温度变化过程
|
||||||
|
steps = min(abs(temperature - start_temp) * 2, 20) # 计算步数
|
||||||
|
step_time = min(timeout / steps if steps > 0 else 1, 2.0) # 每步最多2秒
|
||||||
|
|
||||||
|
for step in range(int(steps)):
|
||||||
|
progress = (step + 1) / steps
|
||||||
|
self.current_temperature = start_temp + (temperature - start_temp) * progress
|
||||||
|
|
||||||
|
# 记录温度历史
|
||||||
|
self.temperature_history.append(
|
||||||
|
{
|
||||||
|
"timestamp": asyncio.get_event_loop().time(),
|
||||||
|
"temperature": self.current_temperature,
|
||||||
|
"target": self.target_temperature,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await asyncio.sleep(step_time)
|
||||||
|
|
||||||
|
# 保持历史记录不超过100条
|
||||||
|
if len(self.temperature_history) > 100:
|
||||||
|
self.temperature_history.pop(0)
|
||||||
|
|
||||||
|
# 最终设置为目标温度
|
||||||
|
self.current_temperature = temperature
|
||||||
|
self.is_heating = False
|
||||||
|
self.is_cooling = False
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"final_temperature": self.current_temperature,
|
||||||
|
"start_temperature": start_temp,
|
||||||
|
"time_taken": steps * step_time,
|
||||||
|
}
|
||||||
|
|
||||||
|
def enable_pid_control(self, kp: float = 1.0, ki: float = 0.1, kd: float = 0.05) -> bool:
|
||||||
|
"""
|
||||||
|
启用PID控制
|
||||||
|
|
||||||
|
Args:
|
||||||
|
kp: 比例增益
|
||||||
|
ki: 积分增益
|
||||||
|
kd: 微分增益
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 启用是否成功
|
||||||
|
"""
|
||||||
|
self.pid_enabled = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
def run_temperature_program(self, program: List[Dict]) -> bool:
|
||||||
|
"""
|
||||||
|
运行温度程序
|
||||||
|
|
||||||
|
Args:
|
||||||
|
program: 温度程序列表,每个元素包含温度和持续时间
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 程序启动是否成功
|
||||||
|
"""
|
||||||
|
# 模拟程序启动
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 状态查询方法
|
||||||
|
def get_current_temperature(self) -> float:
|
||||||
|
"""获取当前温度 (°C)"""
|
||||||
|
return round(self.current_temperature, 2)
|
||||||
|
|
||||||
|
def get_target_temperature(self) -> float:
|
||||||
|
"""获取目标温度 (°C)"""
|
||||||
|
return self.target_temperature
|
||||||
|
|
||||||
|
def get_heating_status(self) -> bool:
|
||||||
|
"""获取加热状态"""
|
||||||
|
return self.is_heating
|
||||||
|
|
||||||
|
def get_cooling_status(self) -> bool:
|
||||||
|
"""获取制冷状态"""
|
||||||
|
return self.is_cooling
|
||||||
|
|
||||||
|
def get_pid_status(self) -> bool:
|
||||||
|
"""获取PID控制状态"""
|
||||||
|
return self.pid_enabled
|
||||||
|
|
||||||
|
def get_temperature_history(self) -> List[Dict]:
|
||||||
|
"""获取温度历史记录"""
|
||||||
|
return self.temperature_history[-10:] # 返回最近10条记录
|
||||||
|
|
||||||
|
def get_controller_status(self) -> Dict[str, Any]:
|
||||||
|
"""获取控制器完整状态"""
|
||||||
|
return {
|
||||||
|
"controller_id": self.controller_id,
|
||||||
|
"current_temp": self.current_temperature,
|
||||||
|
"target_temp": self.target_temperature,
|
||||||
|
"is_heating": self.is_heating,
|
||||||
|
"is_cooling": self.is_cooling,
|
||||||
|
"pid_enabled": self.pid_enabled,
|
||||||
|
"history_count": len(self.temperature_history),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MultiChannelAnalyzer:
|
||||||
|
"""
|
||||||
|
多通道分析仪
|
||||||
|
|
||||||
|
支持同时监测多个通道的信号,提供实时数据采集和分析功能。
|
||||||
|
常用于光谱分析、电化学测量等应用场景。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, analyzer_id: str = "analyzer_01", channels: int = 8):
|
||||||
|
"""
|
||||||
|
初始化多通道分析仪
|
||||||
|
|
||||||
|
Args:
|
||||||
|
analyzer_id: 分析仪ID
|
||||||
|
channels: 通道数量
|
||||||
|
"""
|
||||||
|
self.analyzer_id = analyzer_id
|
||||||
|
self.channel_count = channels
|
||||||
|
self.channel_data = {i: {"value": 0.0, "unit": "V", "enabled": True} for i in range(channels)}
|
||||||
|
self.is_measuring = False
|
||||||
|
self.sample_rate = 1000 # Hz
|
||||||
|
|
||||||
|
def configure_channel(self, channel: int, enabled: bool = True, unit: str = "V") -> bool:
|
||||||
|
"""
|
||||||
|
配置通道
|
||||||
|
|
||||||
|
Args:
|
||||||
|
channel: 通道编号
|
||||||
|
enabled: 是否启用
|
||||||
|
unit: 测量单位
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 配置是否成功
|
||||||
|
"""
|
||||||
|
if 0 <= channel < self.channel_count:
|
||||||
|
self.channel_data[channel]["enabled"] = enabled
|
||||||
|
self.channel_data[channel]["unit"] = unit
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def start_measurement_async(self, duration: int = 10) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
开始异步测量
|
||||||
|
|
||||||
|
Args:
|
||||||
|
duration: 测量持续时间(秒)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict: 测量结果
|
||||||
|
"""
|
||||||
|
self.is_measuring = True
|
||||||
|
|
||||||
|
# 模拟数据采集
|
||||||
|
measurements = []
|
||||||
|
for second in range(duration):
|
||||||
|
timestamp = asyncio.get_event_loop().time()
|
||||||
|
frame_data = {}
|
||||||
|
|
||||||
|
for channel in range(self.channel_count):
|
||||||
|
if self.channel_data[channel]["enabled"]:
|
||||||
|
# 模拟传感器数据
|
||||||
|
import random
|
||||||
|
|
||||||
|
value = random.uniform(-5.0, 5.0)
|
||||||
|
frame_data[f"channel_{channel}"] = value
|
||||||
|
self.channel_data[channel]["value"] = value
|
||||||
|
|
||||||
|
measurements.append({"timestamp": timestamp, "data": frame_data})
|
||||||
|
|
||||||
|
await asyncio.sleep(1.0) # 每秒采集一次
|
||||||
|
|
||||||
|
self.is_measuring = False
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"duration": duration,
|
||||||
|
"samples_count": len(measurements),
|
||||||
|
"measurements": measurements[-5:], # 只返回最后5个样本
|
||||||
|
"channels_active": len([ch for ch in self.channel_data.values() if ch["enabled"]]),
|
||||||
|
}
|
||||||
|
|
||||||
|
def stop_measurement(self) -> bool:
|
||||||
|
"""
|
||||||
|
停止测量
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 停止是否成功
|
||||||
|
"""
|
||||||
|
self.is_measuring = False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def reset_channels(self) -> bool:
|
||||||
|
"""
|
||||||
|
重置所有通道
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 重置是否成功
|
||||||
|
"""
|
||||||
|
for channel in self.channel_data:
|
||||||
|
self.channel_data[channel]["value"] = 0.0
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 状态查询方法
|
||||||
|
def get_measurement_status(self) -> bool:
|
||||||
|
"""获取测量状态"""
|
||||||
|
return self.is_measuring
|
||||||
|
|
||||||
|
def get_channel_count(self) -> int:
|
||||||
|
"""获取通道数量"""
|
||||||
|
return self.channel_count
|
||||||
|
|
||||||
|
def get_sample_rate(self) -> float:
|
||||||
|
"""获取采样率 (Hz)"""
|
||||||
|
return self.sample_rate
|
||||||
|
|
||||||
|
def get_channel_values(self) -> Dict[int, float]:
|
||||||
|
"""获取所有通道的当前值"""
|
||||||
|
return {ch: data["value"] for ch, data in self.channel_data.items() if data["enabled"]}
|
||||||
|
|
||||||
|
def get_enabled_channels(self) -> List[int]:
|
||||||
|
"""获取已启用的通道列表"""
|
||||||
|
return [ch for ch, data in self.channel_data.items() if data["enabled"]]
|
||||||
|
|
||||||
|
def get_analyzer_status(self) -> Dict[str, Any]:
|
||||||
|
"""获取分析仪完整状态"""
|
||||||
|
return {
|
||||||
|
"analyzer_id": self.analyzer_id,
|
||||||
|
"channel_count": self.channel_count,
|
||||||
|
"is_measuring": self.is_measuring,
|
||||||
|
"sample_rate": self.sample_rate,
|
||||||
|
"active_channels": len(self.get_enabled_channels()),
|
||||||
|
"channel_data": self.channel_data,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AutomatedDispenser:
|
||||||
|
"""
|
||||||
|
自动分配器
|
||||||
|
|
||||||
|
精确控制固体和液体材料的分配,支持多种分配模式和容器管理。
|
||||||
|
集成称重功能,确保分配精度和重现性。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, dispenser_id: str = "dispenser_01"):
|
||||||
|
"""
|
||||||
|
初始化自动分配器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dispenser_id: 分配器ID
|
||||||
|
"""
|
||||||
|
self.dispenser_id = dispenser_id
|
||||||
|
self.is_ready = True
|
||||||
|
self.current_position = {"x": 0.0, "y": 0.0, "z": 0.0}
|
||||||
|
self.dispensed_total = 0.0
|
||||||
|
self.container_capacity = 1000.0 # mL
|
||||||
|
self.precision_mode = True
|
||||||
|
|
||||||
|
def move_to_position(self, x: float, y: float, z: float) -> bool:
|
||||||
|
"""
|
||||||
|
移动到指定位置
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x: X坐标 (mm)
|
||||||
|
y: Y坐标 (mm)
|
||||||
|
z: Z坐标 (mm)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 移动是否成功
|
||||||
|
"""
|
||||||
|
self.current_position = {"x": x, "y": y, "z": z}
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def dispense_liquid_async(self, volume: float, container_id: str, viscosity: str = "low") -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
异步分配液体
|
||||||
|
|
||||||
|
Args:
|
||||||
|
volume: 分配体积 (mL)
|
||||||
|
container_id: 容器ID
|
||||||
|
viscosity: 液体粘度等级
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict: 分配结果
|
||||||
|
"""
|
||||||
|
if not self.is_ready:
|
||||||
|
return {"success": False, "error": "设备未就绪"}
|
||||||
|
|
||||||
|
if volume <= 0:
|
||||||
|
return {"success": False, "error": "体积必须大于0"}
|
||||||
|
|
||||||
|
# 模拟分配过程
|
||||||
|
dispense_time = volume * 0.1 # 每mL需要0.1秒
|
||||||
|
if viscosity == "high":
|
||||||
|
dispense_time *= 2 # 高粘度液体需要更长时间
|
||||||
|
|
||||||
|
await asyncio.sleep(min(dispense_time, 5.0)) # 最多等待5秒
|
||||||
|
|
||||||
|
self.dispensed_total += volume
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"dispensed_volume": volume,
|
||||||
|
"container_id": container_id,
|
||||||
|
"actual_time": min(dispense_time, 5.0),
|
||||||
|
"total_dispensed": self.dispensed_total,
|
||||||
|
}
|
||||||
|
|
||||||
|
def clean_dispenser(self, wash_volume: float = 5.0) -> bool:
|
||||||
|
"""
|
||||||
|
清洗分配器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
wash_volume: 清洗液体积 (mL)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 清洗是否成功
|
||||||
|
"""
|
||||||
|
# 模拟清洗过程
|
||||||
|
return True
|
||||||
|
|
||||||
|
def calibrate_volume(self, target_volume: float) -> bool:
|
||||||
|
"""
|
||||||
|
校准分配体积
|
||||||
|
|
||||||
|
Args:
|
||||||
|
target_volume: 校准目标体积 (mL)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 校准是否成功
|
||||||
|
"""
|
||||||
|
# 模拟校准过程
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 状态查询方法
|
||||||
|
def get_ready_status(self) -> bool:
|
||||||
|
"""获取就绪状态"""
|
||||||
|
return self.is_ready
|
||||||
|
|
||||||
|
def get_current_position(self) -> Dict[str, float]:
|
||||||
|
"""获取当前位置坐标"""
|
||||||
|
return self.current_position.copy()
|
||||||
|
|
||||||
|
def get_dispensed_total(self) -> float:
|
||||||
|
"""获取累计分配体积 (mL)"""
|
||||||
|
return self.dispensed_total
|
||||||
|
|
||||||
|
def get_container_capacity(self) -> float:
|
||||||
|
"""获取容器容量 (mL)"""
|
||||||
|
return self.container_capacity
|
||||||
|
|
||||||
|
def get_precision_mode(self) -> bool:
|
||||||
|
"""获取精密模式状态"""
|
||||||
|
return self.precision_mode
|
||||||
|
|
||||||
|
def get_dispenser_status(self) -> Dict[str, Any]:
|
||||||
|
"""获取分配器完整状态"""
|
||||||
|
return {
|
||||||
|
"dispenser_id": self.dispenser_id,
|
||||||
|
"ready": self.is_ready,
|
||||||
|
"position": self.current_position,
|
||||||
|
"dispensed_total": self.dispensed_total,
|
||||||
|
"capacity": self.container_capacity,
|
||||||
|
"precision_mode": self.precision_mode,
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package:
|
package:
|
||||||
name: ros-humble-unilabos-msgs
|
name: ros-humble-unilabos-msgs
|
||||||
version: 0.10.3
|
version: 0.10.4
|
||||||
source:
|
source:
|
||||||
path: ../../unilabos_msgs
|
path: ../../unilabos_msgs
|
||||||
target_directory: src
|
target_directory: src
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package:
|
package:
|
||||||
name: unilabos
|
name: unilabos
|
||||||
version: "0.10.3"
|
version: "0.10.4"
|
||||||
|
|
||||||
source:
|
source:
|
||||||
path: ../..
|
path: ../..
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -4,7 +4,7 @@ package_name = 'unilabos'
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name=package_name,
|
name=package_name,
|
||||||
version='0.10.3',
|
version='0.10.4',
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
install_requires=['setuptools'],
|
install_requires=['setuptools'],
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -49,7 +49,6 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"protocol_type": [
|
"protocol_type": [
|
||||||
"AddProtocol",
|
"AddProtocol",
|
||||||
"TransferProtocol",
|
|
||||||
"StartStirProtocol",
|
"StartStirProtocol",
|
||||||
"StopStirProtocol",
|
"StopStirProtocol",
|
||||||
"StirProtocol",
|
"StirProtocol",
|
||||||
@@ -171,12 +170,15 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"volume": 1000.0,
|
"max_volume": 1000.0
|
||||||
"reagent": "DMF"
|
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"current_volume": 1000.0,
|
"liquids": [
|
||||||
"reagent_name": "DMF"
|
{
|
||||||
|
"liquid_type": "DMF",
|
||||||
|
"liquid_volume": 1000.0
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -192,12 +194,15 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"volume": 1000.0,
|
"max_volume": 1000.0
|
||||||
"reagent": "ethyl_acetate"
|
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"current_volume": 1000.0,
|
"liquids": [
|
||||||
"reagent_name": "ethyl_acetate"
|
{
|
||||||
|
"liquid_type": "ethyl_acetate",
|
||||||
|
"liquid_volume": 1000.0
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -213,12 +218,15 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"volume": 1000.0,
|
"max_volume": 1000.0
|
||||||
"reagent": "hexane"
|
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"current_volume": 1000.0,
|
"liquids": [
|
||||||
"reagent_name": "hexane"
|
{
|
||||||
|
"liquid_type": "hexane",
|
||||||
|
"liquid_volume": 1000.0
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -234,12 +242,15 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"volume": 1000.0,
|
"max_volume": 1000.0
|
||||||
"reagent": "methanol"
|
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"current_volume": 1000.0,
|
"liquids": [
|
||||||
"reagent_name": "methanol"
|
{
|
||||||
|
"liquid_type": "methanol",
|
||||||
|
"liquid_volume": 1000.0
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -255,12 +266,15 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"volume": 1000.0,
|
"max_volume": 1000.0
|
||||||
"reagent": "water"
|
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"current_volume": 1000.0,
|
"liquids": [
|
||||||
"reagent_name": "water"
|
{
|
||||||
|
"liquid_type": "water",
|
||||||
|
"liquid_volume": 1000.0
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -320,15 +334,15 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"volume": 500.0,
|
"max_volume": 500.0,
|
||||||
"max_temp": 200.0,
|
"max_temp": 200.0,
|
||||||
"min_temp": -20.0,
|
"min_temp": -20.0,
|
||||||
"has_stirrer": true,
|
"has_stirrer": true,
|
||||||
"has_heater": true
|
"has_heater": true
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"current_volume": 0.0,
|
"liquids": [
|
||||||
"current_temp": 25.0
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -405,10 +419,11 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"volume": 2000.0
|
"max_volume": 2000.0
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"current_volume": 0.0
|
"liquids": [
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -424,10 +439,11 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"volume": 2000.0
|
"max_volume": 2000.0
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"current_volume": 0.0
|
"liquids": [
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -633,10 +649,11 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"volume": 250.0
|
"max_volume": 250.0
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"current_volume": 0.0
|
"liquids": [
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -652,10 +669,11 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"volume": 250.0
|
"max_volume": 250.0
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"current_volume": 0.0
|
"liquids": [
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -671,10 +689,11 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"volume": 250.0
|
"max_volume": 250.0
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"current_volume": 0.0
|
"liquids": [
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -713,7 +732,7 @@
|
|||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"volume": 500.0,
|
"max_volume": 500.0,
|
||||||
"reagent": "sodium_chloride",
|
"reagent": "sodium_chloride",
|
||||||
"physical_state": "solid"
|
"physical_state": "solid"
|
||||||
},
|
},
|
||||||
@@ -1077,7 +1096,7 @@
|
|||||||
"target": "solid_dispenser_1",
|
"target": "solid_dispenser_1",
|
||||||
"type": "resource",
|
"type": "resource",
|
||||||
"port": {
|
"port": {
|
||||||
"solid_reagent_bottle_1": "top",
|
"solid_reagent_bottle_1": "bottom",
|
||||||
"solid_dispenser_1": "SolidIn"
|
"solid_dispenser_1": "SolidIn"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1087,7 +1106,7 @@
|
|||||||
"target": "solid_dispenser_1",
|
"target": "solid_dispenser_1",
|
||||||
"type": "resource",
|
"type": "resource",
|
||||||
"port": {
|
"port": {
|
||||||
"solid_reagent_bottle_2": "top",
|
"solid_reagent_bottle_2": "bottom",
|
||||||
"solid_dispenser_1": "SolidIn"
|
"solid_dispenser_1": "SolidIn"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1097,7 +1116,7 @@
|
|||||||
"target": "solid_dispenser_1",
|
"target": "solid_dispenser_1",
|
||||||
"type": "resource",
|
"type": "resource",
|
||||||
"port": {
|
"port": {
|
||||||
"solid_reagent_bottle_3": "top",
|
"solid_reagent_bottle_3": "bottom",
|
||||||
"solid_dispenser_1": "SolidIn"
|
"solid_dispenser_1": "SolidIn"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,8 @@
|
|||||||
"type": "device",
|
"type": "device",
|
||||||
"class": "workstation",
|
"class": "workstation",
|
||||||
"position": {
|
"position": {
|
||||||
"x": 620.6111111111111,
|
"x": 0,
|
||||||
"y": 171,
|
"y": 0,
|
||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
"id": "PLR_STATION",
|
"id": "liquid_handler",
|
||||||
"name": "PLR_LH_TEST",
|
"name": "liquid_handler",
|
||||||
"parent": null,
|
"parent": null,
|
||||||
"type": "device",
|
"type": "device",
|
||||||
"class": "liquid_handler",
|
"class": "liquid_handler",
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
"tip_rack",
|
"tip_rack",
|
||||||
"plate_well"
|
"plate_well"
|
||||||
],
|
],
|
||||||
"parent": "PLR_STATION",
|
"parent": "liquid_handler",
|
||||||
"type": "deck",
|
"type": "deck",
|
||||||
"class": "OTDeck",
|
"class": "OTDeck",
|
||||||
"position": {
|
"position": {
|
||||||
@@ -9650,7 +9650,7 @@
|
|||||||
"children": [],
|
"children": [],
|
||||||
"parent": null,
|
"parent": null,
|
||||||
"type": "device",
|
"type": "device",
|
||||||
"class": "moveit.arm_slider",
|
"class": "robotic_arm.SCARA_with_slider.virtual",
|
||||||
"position": {
|
"position": {
|
||||||
"x": -500,
|
"x": -500,
|
||||||
"y": 1000,
|
"y": 1000,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"children": [],
|
"children": [],
|
||||||
"parent": null,
|
"parent": null,
|
||||||
"type": "device",
|
"type": "device",
|
||||||
"class": "moveit.arm_slider",
|
"class": "robotic_arm.SCARA_with_slider.virtual",
|
||||||
"position": {
|
"position": {
|
||||||
"x": -500,
|
"x": -500,
|
||||||
"y": 1000,
|
"y": 1000,
|
||||||
|
|||||||
949
test/experiments/workshop.json
Normal file
949
test/experiments/workshop.json
Normal file
@@ -0,0 +1,949 @@
|
|||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "simple_station",
|
||||||
|
"name": "愚公常量合成工作站",
|
||||||
|
"children": [
|
||||||
|
"serial_pump",
|
||||||
|
"pump_reagents",
|
||||||
|
"pump_workup",
|
||||||
|
"flask_CH2Cl2",
|
||||||
|
"waste_workup",
|
||||||
|
"separator_controller",
|
||||||
|
"flask_separator",
|
||||||
|
"flask_air"
|
||||||
|
],
|
||||||
|
"parent": null,
|
||||||
|
"type": "device",
|
||||||
|
"class": "workstation",
|
||||||
|
"position": {
|
||||||
|
"x": 620.6111111111111,
|
||||||
|
"y": 171,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"protocol_type": ["PumpTransferProtocol", "CleanProtocol", "SeparateProtocol", "EvaporateProtocol"]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "serial_pump",
|
||||||
|
"name": "serial_pump",
|
||||||
|
"children": [],
|
||||||
|
"parent": "simple_station",
|
||||||
|
"type": "device",
|
||||||
|
"class": "serial",
|
||||||
|
"position": {
|
||||||
|
"x": 620.6111111111111,
|
||||||
|
"y": 171,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"port": "COM7",
|
||||||
|
"baudrate": 9600
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pump_reagents",
|
||||||
|
"name": "pump_reagents",
|
||||||
|
"children": [],
|
||||||
|
"parent": "simple_station",
|
||||||
|
"type": "device",
|
||||||
|
"class": "syringepump.runze",
|
||||||
|
"position": {
|
||||||
|
"x": 620.6111111111111,
|
||||||
|
"y": 171,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"port": "/devices/PumpBackbone/Serial/serialwrite",
|
||||||
|
"address": "1",
|
||||||
|
"max_volume": 25.0
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"max_velocity": 1.0,
|
||||||
|
"position": 0.0,
|
||||||
|
"status": "Idle",
|
||||||
|
"valve_position": "0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flask_CH2Cl2",
|
||||||
|
"name": "flask_CH2Cl2",
|
||||||
|
"children": [],
|
||||||
|
"parent": "simple_station",
|
||||||
|
"type": "container",
|
||||||
|
"class": null,
|
||||||
|
"position": {
|
||||||
|
"x": 430.4087301587302,
|
||||||
|
"y": 428,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"max_volume": 2000.0
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"liquid": [
|
||||||
|
{
|
||||||
|
"liquid_type": "CH2Cl2",
|
||||||
|
"liquid_volume": 1500.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flask_acetone",
|
||||||
|
"name": "flask_acetone",
|
||||||
|
"children": [],
|
||||||
|
"parent": "simple_station",
|
||||||
|
"type": "container",
|
||||||
|
"class": null,
|
||||||
|
"position": {
|
||||||
|
"x": 295.36944444444447,
|
||||||
|
"y": 428,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"max_volume": 2000.0
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"liquid": [
|
||||||
|
{
|
||||||
|
"liquid_type": "acetone",
|
||||||
|
"liquid_volume": 1500.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flask_NH4Cl",
|
||||||
|
"name": "flask_NH4Cl",
|
||||||
|
"children": [],
|
||||||
|
"parent": "simple_station",
|
||||||
|
"type": "container",
|
||||||
|
"class": null,
|
||||||
|
"position": {
|
||||||
|
"x": 165.36944444444444,
|
||||||
|
"y": 428,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"max_volume": 2000.0
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"liquid": [
|
||||||
|
{
|
||||||
|
"liquid_type": "NH4Cl",
|
||||||
|
"liquid_volume": 1500.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flask_grignard",
|
||||||
|
"name": "flask_grignard",
|
||||||
|
"children": [],
|
||||||
|
"parent": "simple_station",
|
||||||
|
"type": "container",
|
||||||
|
"class": null,
|
||||||
|
"position": {
|
||||||
|
"x": 165.36944444444444,
|
||||||
|
"y": 428,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"max_volume": 2000.0
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"liquid": [
|
||||||
|
{
|
||||||
|
"liquid_type": "grignard",
|
||||||
|
"liquid_volume": 1500.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flask_THF",
|
||||||
|
"name": "flask_THF",
|
||||||
|
"children": [],
|
||||||
|
"parent": "simple_station",
|
||||||
|
"type": "container",
|
||||||
|
"class": null,
|
||||||
|
"position": {
|
||||||
|
"x": 35,
|
||||||
|
"y": 428,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"max_volume": 2000.0
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"liquid": [
|
||||||
|
{
|
||||||
|
"liquid_type": "THF",
|
||||||
|
"liquid_volume": 1500.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "reactor",
|
||||||
|
"name": "reactor",
|
||||||
|
"children": [],
|
||||||
|
"parent": "simple_station",
|
||||||
|
"type": "container",
|
||||||
|
"class": null,
|
||||||
|
"position": {
|
||||||
|
"x": 698.1111111111111,
|
||||||
|
"y": 428,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"max_volume": 5000.0
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"liquid": [
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "stirrer",
|
||||||
|
"name": "stirrer",
|
||||||
|
"children": [],
|
||||||
|
"parent": "simple_station",
|
||||||
|
"type": "device",
|
||||||
|
"class": "heaterstirrer.dalong",
|
||||||
|
"position": {
|
||||||
|
"x": 698.1111111111111,
|
||||||
|
"y": 478,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"port": "COM43",
|
||||||
|
"temp_warning": 60.0
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"status": "Idle",
|
||||||
|
"temp": 0.0,
|
||||||
|
"stir_speed": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pump_workup",
|
||||||
|
"name": "pump_workup",
|
||||||
|
"children": [],
|
||||||
|
"parent": "simple_station",
|
||||||
|
"type": "device",
|
||||||
|
"class": "syringepump.runze",
|
||||||
|
"position": {
|
||||||
|
"x": 1195.611507936508,
|
||||||
|
"y": 686,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"port": "/devices/PumpBackbone/Serial/serialwrite",
|
||||||
|
"address": "2",
|
||||||
|
"max_volume": 25.0
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"max_velocity": 1.0,
|
||||||
|
"position": 0.0,
|
||||||
|
"status": "Idle",
|
||||||
|
"valve_position": "0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "waste_workup",
|
||||||
|
"name": "waste_workup",
|
||||||
|
"children": [],
|
||||||
|
"parent": "simple_station",
|
||||||
|
"type": "container",
|
||||||
|
"class": null,
|
||||||
|
"position": {
|
||||||
|
"x": 1587.703373015873,
|
||||||
|
"y": 1172.5,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"max_volume": 2000.0
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"liquid": [
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "separator_controller",
|
||||||
|
"name": "separator_controller",
|
||||||
|
"children": [],
|
||||||
|
"parent": "simple_station",
|
||||||
|
"type": "device",
|
||||||
|
"class": "separator.homemade",
|
||||||
|
"position": {
|
||||||
|
"x": 1624.4027777777778,
|
||||||
|
"y": 665.5,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"port_executor": "/dev/tty.usbserial-11140",
|
||||||
|
"port_sensor": "/dev/tty.usbserial-11130"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"sensordata": 0.0,
|
||||||
|
"status": "Idle"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flask_separator",
|
||||||
|
"name": "flask_separator",
|
||||||
|
"children": [],
|
||||||
|
"parent": "simple_station",
|
||||||
|
"type": "container",
|
||||||
|
"class": null,
|
||||||
|
"position": {
|
||||||
|
"x": 1614.404365079365,
|
||||||
|
"y": 948,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"max_volume": 2000.0
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"liquid": [
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flask_holding",
|
||||||
|
"name": "flask_holding",
|
||||||
|
"children": [],
|
||||||
|
"parent": "simple_station",
|
||||||
|
"type": "container",
|
||||||
|
"class": null,
|
||||||
|
"position": {
|
||||||
|
"x": 1915.7035714285714,
|
||||||
|
"y": 665.5,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"max_volume": 2000.0
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"liquid": [
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flask_H2O",
|
||||||
|
"name": "flask_H2O",
|
||||||
|
"children": [],
|
||||||
|
"parent": "simple_station",
|
||||||
|
"type": "container",
|
||||||
|
"class": null,
|
||||||
|
"position": {
|
||||||
|
"x": 1785.7035714285714,
|
||||||
|
"y": 665.5,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"max_volume": 2000.0
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"liquid": [
|
||||||
|
{
|
||||||
|
"liquid_type": "H2O",
|
||||||
|
"liquid_volume": 1500.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flask_NaHCO3",
|
||||||
|
"name": "flask_NaHCO3",
|
||||||
|
"children": [],
|
||||||
|
"parent": "simple_station",
|
||||||
|
"type": "container",
|
||||||
|
"class": null,
|
||||||
|
"position": {
|
||||||
|
"x": 2054.0650793650793,
|
||||||
|
"y": 665.5,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"max_volume": 2000.0
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"liquid": [
|
||||||
|
{
|
||||||
|
"liquid_type": "NaHCO3",
|
||||||
|
"liquid_volume": 1500.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pump_column",
|
||||||
|
"name": "pump_column",
|
||||||
|
"children": [],
|
||||||
|
"parent": "simple_station",
|
||||||
|
"type": "device",
|
||||||
|
"class": "syringepump.runze",
|
||||||
|
"position": {
|
||||||
|
"x": 1630.6527777777778,
|
||||||
|
"y": 448.5,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"port": "/devices/PumpBackbone/Serial/serialwrite",
|
||||||
|
"address": "3",
|
||||||
|
"max_volume": 25.0
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"max_velocity": 1.0,
|
||||||
|
"position": 0.0,
|
||||||
|
"status": "Idle",
|
||||||
|
"valve_position": "0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rotavap",
|
||||||
|
"name": "rotavap",
|
||||||
|
"children": [],
|
||||||
|
"parent": "simple_station",
|
||||||
|
"type": "device",
|
||||||
|
"class": "rotavap",
|
||||||
|
"position": {
|
||||||
|
"x": 1339.7031746031746,
|
||||||
|
"y": 968.5,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"port": "COM15"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"temperature": 0.0,
|
||||||
|
"rotate_time": 0.0,
|
||||||
|
"status": "Idle"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flask_rv",
|
||||||
|
"name": "flask_rv",
|
||||||
|
"children": [],
|
||||||
|
"parent": "simple_station",
|
||||||
|
"type": "container",
|
||||||
|
"class": null,
|
||||||
|
"position": {
|
||||||
|
"x": 1339.7031746031746,
|
||||||
|
"y": 1152,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"max_volume": 2000.0
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"liquid": [
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "column",
|
||||||
|
"name": "column",
|
||||||
|
"children": [],
|
||||||
|
"parent": "simple_station",
|
||||||
|
"type": "container",
|
||||||
|
"class": null,
|
||||||
|
"position": {
|
||||||
|
"x": 909.722619047619,
|
||||||
|
"y": 948,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"max_volume": 200.0
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"liquid": [
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flask_column",
|
||||||
|
"name": "flask_column",
|
||||||
|
"children": [],
|
||||||
|
"parent": "simple_station",
|
||||||
|
"type": "container",
|
||||||
|
"class": null,
|
||||||
|
"position": {
|
||||||
|
"x": 867.972619047619,
|
||||||
|
"y": 1152,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"max_volume": 2000.0
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"liquid": [
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flask_air",
|
||||||
|
"name": "flask_air",
|
||||||
|
"children": [],
|
||||||
|
"parent": "simple_station",
|
||||||
|
"type": "container",
|
||||||
|
"class": null,
|
||||||
|
"position": {
|
||||||
|
"x": 742.722619047619,
|
||||||
|
"y": 948,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"max_volume": 2000.0
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"liquid": [
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dry_column",
|
||||||
|
"name": "dry_column",
|
||||||
|
"children": [],
|
||||||
|
"parent": "simple_station",
|
||||||
|
"type": "container",
|
||||||
|
"class": null,
|
||||||
|
"position": {
|
||||||
|
"x": 1206.722619047619,
|
||||||
|
"y": 948,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"max_volume": 200.0
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"liquid": [
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "flask_dry_column",
|
||||||
|
"name": "flask_dry_column",
|
||||||
|
"children": [],
|
||||||
|
"parent": "simple_station",
|
||||||
|
"type": "container",
|
||||||
|
"class": null,
|
||||||
|
"position": {
|
||||||
|
"x": 1148.222619047619,
|
||||||
|
"y": 1152,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"max_volume": 2000.0
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"liquid": [
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pump_ext",
|
||||||
|
"name": "pump_ext",
|
||||||
|
"children": [],
|
||||||
|
"parent": "simple_station",
|
||||||
|
"type": "device",
|
||||||
|
"class": "syringepump.runze",
|
||||||
|
"position": {
|
||||||
|
"x": 1469.7031746031746,
|
||||||
|
"y": 968.5,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"port": "/devices/PumpBackbone/Serial/serialwrite",
|
||||||
|
"address": "4",
|
||||||
|
"max_volume": 25.0
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"max_velocity": 1.0,
|
||||||
|
"position": 0.0,
|
||||||
|
"status": "Idle",
|
||||||
|
"valve_position": "0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "AGV",
|
||||||
|
"name": "AGV",
|
||||||
|
"children": ["zhixing_agv", "zhixing_ur_arm"],
|
||||||
|
"parent": null,
|
||||||
|
"type": "device",
|
||||||
|
"class": "workstation",
|
||||||
|
"position": {
|
||||||
|
"x": 698.1111111111111,
|
||||||
|
"y": 478,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"protocol_type": ["AGVTransferProtocol"]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "zhixing_agv",
|
||||||
|
"name": "zhixing_agv",
|
||||||
|
"children": [],
|
||||||
|
"parent": "AGV",
|
||||||
|
"type": "device",
|
||||||
|
"class": "zhixing_agv",
|
||||||
|
"position": {
|
||||||
|
"x": 698.1111111111111,
|
||||||
|
"y": 478,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"host": "192.168.1.42"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "zhixing_ur_arm",
|
||||||
|
"name": "zhixing_ur_arm",
|
||||||
|
"children": [],
|
||||||
|
"parent": "AGV",
|
||||||
|
"type": "device",
|
||||||
|
"class": "zhixing_ur_arm",
|
||||||
|
"position": {
|
||||||
|
"x": 698.1111111111111,
|
||||||
|
"y": 478,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"host": "192.168.1.178"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"source": "pump_reagents",
|
||||||
|
"target": "serial_pump",
|
||||||
|
"type": "communication",
|
||||||
|
"port": {
|
||||||
|
"pump_reagents": "port",
|
||||||
|
"serial_pump": "port"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "pump_workup",
|
||||||
|
"target": "serial_pump",
|
||||||
|
"type": "communication",
|
||||||
|
"port": {
|
||||||
|
"pump_reagents": "port",
|
||||||
|
"serial_pump": "port"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "pump_column",
|
||||||
|
"target": "serial_pump",
|
||||||
|
"type": "communication",
|
||||||
|
"port": {
|
||||||
|
"pump_reagents": "port",
|
||||||
|
"serial_pump": "port"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "pump_ext",
|
||||||
|
"target": "serial_pump",
|
||||||
|
"type": "communication",
|
||||||
|
"port": {
|
||||||
|
"pump_reagents": "port",
|
||||||
|
"serial_pump": "port"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "reactor",
|
||||||
|
"target": "pump_reagents",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"reactor": "top",
|
||||||
|
"pump_reagents": "5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "rotavap",
|
||||||
|
"target": "flask_rv",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"rotavap": "bottom",
|
||||||
|
"flask_rv": "top"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "separator_controller",
|
||||||
|
"target": "flask_separator",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"separator_controller": "bottom",
|
||||||
|
"flask_separator": "top"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "column",
|
||||||
|
"target": "flask_column",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"column": "bottom",
|
||||||
|
"flask_column": "top"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "dry_column",
|
||||||
|
"target": "flask_dry_column",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"dry_column": "bottom",
|
||||||
|
"flask_dry_column": "top"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "pump_ext",
|
||||||
|
"target": "pump_column",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"pump_ext": "8",
|
||||||
|
"pump_column": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "pump_ext",
|
||||||
|
"target": "waste_workup",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"pump_ext": "2",
|
||||||
|
"waste_workup": "-1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "pump_reagents",
|
||||||
|
"target": "flask_THF",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"pump_reagents": "7",
|
||||||
|
"flask_THF": "top"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "pump_reagents",
|
||||||
|
"target": "flask_NH4Cl",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"pump_reagents": "4",
|
||||||
|
"flask_NH4Cl": "top"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "pump_reagents",
|
||||||
|
"target": "flask_CH2Cl2",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"pump_reagents": "2",
|
||||||
|
"flask_CH2Cl2": "top"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "pump_reagents",
|
||||||
|
"target": "flask_acetone",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"pump_reagents": "3",
|
||||||
|
"flask_acetone": "top"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "pump_reagents",
|
||||||
|
"target": "pump_workup",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"pump_reagents": "1",
|
||||||
|
"pump_workup": "8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "pump_reagents",
|
||||||
|
"target": "flask_grignard",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"pump_reagents": "6",
|
||||||
|
"flask_grignard": "top"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "pump_reagents",
|
||||||
|
"target": "reactor",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"pump_reagents": "5",
|
||||||
|
"reactor": "top"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "pump_reagents",
|
||||||
|
"target": "flask_air",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"pump_reagents": "8",
|
||||||
|
"flask_air": "-1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "pump_workup",
|
||||||
|
"target": "waste_workup",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"pump_workup": "2",
|
||||||
|
"waste_workup": "-1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "pump_workup",
|
||||||
|
"target": "flask_H2O",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"pump_workup": "7",
|
||||||
|
"flask_H2O": "top"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "pump_workup",
|
||||||
|
"target": "flask_NaHCO3",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"pump_workup": "6",
|
||||||
|
"flask_NaHCO3": "top"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "pump_workup",
|
||||||
|
"target": "pump_reagents",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"pump_workup": "8",
|
||||||
|
"pump_reagents": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "pump_workup",
|
||||||
|
"target": "flask_holding",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"pump_workup": "5",
|
||||||
|
"flask_holding": "top"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "pump_workup",
|
||||||
|
"target": "separator_controller",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"pump_workup": "4",
|
||||||
|
"separator_controller": "top"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "pump_workup",
|
||||||
|
"target": "flask_separator",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"pump_workup": "3",
|
||||||
|
"flask_separator": "top"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "pump_workup",
|
||||||
|
"target": "pump_column",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"pump_workup": "1",
|
||||||
|
"pump_column": "8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "pump_column",
|
||||||
|
"target": "column",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"pump_column": "4",
|
||||||
|
"column": "top"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "pump_column",
|
||||||
|
"target": "flask_column",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"pump_column": "3",
|
||||||
|
"flask_column": "top"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "pump_column",
|
||||||
|
"target": "rotavap",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"pump_column": "2",
|
||||||
|
"rotavap": "-1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "pump_column",
|
||||||
|
"target": "pump_workup",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"pump_column": "8",
|
||||||
|
"pump_workup": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "pump_column",
|
||||||
|
"target": "flask_air",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"pump_column": "5",
|
||||||
|
"flask_air": "-1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "pump_column",
|
||||||
|
"target": "dry_column",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"pump_column": "7",
|
||||||
|
"dry_column": "top"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "pump_column",
|
||||||
|
"target": "flask_dry_column",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"pump_column": "6",
|
||||||
|
"flask_dry_column": "top"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "pump_column",
|
||||||
|
"target": "pump_ext",
|
||||||
|
"type": "physical",
|
||||||
|
"port": {
|
||||||
|
"pump_column": "1",
|
||||||
|
"pump_ext": "8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ dependencies:
|
|||||||
- uvicorn
|
- uvicorn
|
||||||
- gradio
|
- gradio
|
||||||
- flask
|
- flask
|
||||||
- websocket
|
- websockets
|
||||||
# Notebook
|
# Notebook
|
||||||
- ipython
|
- ipython
|
||||||
- jupyter
|
- jupyter
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ dependencies:
|
|||||||
- uvicorn
|
- uvicorn
|
||||||
- gradio
|
- gradio
|
||||||
- flask
|
- flask
|
||||||
- websocket
|
- websockets
|
||||||
# Notebook
|
# Notebook
|
||||||
- ipython
|
- ipython
|
||||||
- jupyter
|
- jupyter
|
||||||
|
|||||||
@@ -35,8 +35,7 @@ dependencies:
|
|||||||
- uvicorn
|
- uvicorn
|
||||||
- gradio
|
- gradio
|
||||||
- flask
|
- flask
|
||||||
- websocket
|
- websockets
|
||||||
- paho-mqtt
|
|
||||||
# Notebook
|
# Notebook
|
||||||
- ipython
|
- ipython
|
||||||
- jupyter
|
- jupyter
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ dependencies:
|
|||||||
- uvicorn
|
- uvicorn
|
||||||
- gradio
|
- gradio
|
||||||
- flask
|
- flask
|
||||||
- websocket
|
- websockets
|
||||||
# Notebook
|
# Notebook
|
||||||
- ipython
|
- ipython
|
||||||
- jupyter
|
- jupyter
|
||||||
|
|||||||
204
unilabos/app/communication.py
Normal file
204
unilabos/app/communication.py
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# coding=utf-8
|
||||||
|
"""
|
||||||
|
通信模块
|
||||||
|
|
||||||
|
提供MQTT和WebSocket的统一接口,支持通过配置选择通信协议。
|
||||||
|
包含通信抽象层基类和通信客户端工厂。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Optional
|
||||||
|
from unilabos.config.config import BasicConfig
|
||||||
|
from unilabos.utils import logger
|
||||||
|
|
||||||
|
|
||||||
|
class BaseCommunicationClient(ABC):
|
||||||
|
"""
|
||||||
|
通信客户端抽象基类
|
||||||
|
|
||||||
|
定义了所有通信客户端(MQTT、WebSocket等)需要实现的接口。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.is_disabled = True
|
||||||
|
self.client_id = ""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def start(self) -> None:
|
||||||
|
"""
|
||||||
|
启动通信客户端连接
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def stop(self) -> None:
|
||||||
|
"""
|
||||||
|
停止通信客户端连接
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def publish_device_status(self, device_status: dict, device_id: str, property_name: str) -> None:
|
||||||
|
"""
|
||||||
|
发布设备状态信息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
device_status: 设备状态字典
|
||||||
|
device_id: 设备ID
|
||||||
|
property_name: 属性名称
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def publish_job_status(
|
||||||
|
self, feedback_data: dict, job_id: str, status: str, return_info: Optional[str] = None
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
发布作业状态信息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
feedback_data: 反馈数据
|
||||||
|
job_id: 作业ID
|
||||||
|
status: 作业状态
|
||||||
|
return_info: 返回信息
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def send_ping(self, ping_id: str, timestamp: float) -> None:
|
||||||
|
"""
|
||||||
|
发送ping消息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ping_id: ping ID
|
||||||
|
timestamp: 时间戳
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def setup_pong_subscription(self) -> None:
|
||||||
|
"""
|
||||||
|
设置pong消息订阅(可选实现)
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_connected(self) -> bool:
|
||||||
|
"""
|
||||||
|
检查是否已连接
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否已连接
|
||||||
|
"""
|
||||||
|
return not self.is_disabled
|
||||||
|
|
||||||
|
|
||||||
|
class CommunicationClientFactory:
|
||||||
|
"""
|
||||||
|
通信客户端工厂类
|
||||||
|
|
||||||
|
根据配置文件中的通信协议设置创建相应的客户端实例。
|
||||||
|
"""
|
||||||
|
|
||||||
|
_client_cache: Optional[BaseCommunicationClient] = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_client(cls, protocol: Optional[str] = None) -> BaseCommunicationClient:
|
||||||
|
"""
|
||||||
|
创建通信客户端实例
|
||||||
|
|
||||||
|
Args:
|
||||||
|
protocol: 指定的协议类型,如果为None则使用配置文件中的设置
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
通信客户端实例
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: 当协议类型不支持时
|
||||||
|
"""
|
||||||
|
if protocol is None:
|
||||||
|
protocol = BasicConfig.communication_protocol
|
||||||
|
|
||||||
|
protocol = protocol.lower()
|
||||||
|
|
||||||
|
if protocol == "mqtt":
|
||||||
|
return cls._create_mqtt_client()
|
||||||
|
elif protocol == "websocket":
|
||||||
|
return cls._create_websocket_client()
|
||||||
|
else:
|
||||||
|
logger.error(f"[CommunicationFactory] Unsupported protocol: {protocol}")
|
||||||
|
logger.warning(f"[CommunicationFactory] Falling back to MQTT")
|
||||||
|
return cls._create_mqtt_client()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_client(cls, protocol: Optional[str] = None) -> BaseCommunicationClient:
|
||||||
|
"""
|
||||||
|
获取通信客户端实例(单例模式)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
protocol: 指定的协议类型,如果为None则使用配置文件中的设置
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
通信客户端实例
|
||||||
|
"""
|
||||||
|
if cls._client_cache is None:
|
||||||
|
cls._client_cache = cls.create_client(protocol)
|
||||||
|
logger.info(f"[CommunicationFactory] Created {type(cls._client_cache).__name__} client")
|
||||||
|
|
||||||
|
return cls._client_cache
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _create_mqtt_client(cls) -> BaseCommunicationClient:
|
||||||
|
"""创建MQTT客户端"""
|
||||||
|
try:
|
||||||
|
from unilabos.app.mq import mqtt_client
|
||||||
|
return mqtt_client
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[CommunicationFactory] Failed to create MQTT client: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _create_websocket_client(cls) -> BaseCommunicationClient:
|
||||||
|
"""创建WebSocket客户端"""
|
||||||
|
try:
|
||||||
|
from unilabos.app.ws_client import WebSocketClient
|
||||||
|
return WebSocketClient()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[CommunicationFactory] Failed to create WebSocket client: {str(e)}")
|
||||||
|
logger.warning(f"[CommunicationFactory] Falling back to MQTT")
|
||||||
|
return cls._create_mqtt_client()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def reset_client(cls):
|
||||||
|
"""重置客户端缓存(用于测试或重新配置)"""
|
||||||
|
if cls._client_cache:
|
||||||
|
try:
|
||||||
|
cls._client_cache.stop()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[CommunicationFactory] Error stopping old client: {str(e)}")
|
||||||
|
|
||||||
|
cls._client_cache = None
|
||||||
|
logger.info("[CommunicationFactory] Client cache reset")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_supported_protocols(cls) -> list[str]:
|
||||||
|
"""
|
||||||
|
获取支持的协议列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
支持的协议列表
|
||||||
|
"""
|
||||||
|
return ["mqtt", "websocket"]
|
||||||
|
|
||||||
|
|
||||||
|
def get_communication_client(protocol: Optional[str] = None) -> BaseCommunicationClient:
|
||||||
|
"""
|
||||||
|
获取通信客户端实例的便捷函数
|
||||||
|
|
||||||
|
Args:
|
||||||
|
protocol: 指定的协议类型,如果为None则使用配置文件中的设置
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
通信客户端实例
|
||||||
|
"""
|
||||||
|
return CommunicationClientFactory.get_client(protocol)
|
||||||
@@ -10,7 +10,6 @@ from copy import deepcopy
|
|||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from unilabos.resources.graphio import modify_to_backend_format
|
|
||||||
|
|
||||||
# 首先添加项目根目录到路径
|
# 首先添加项目根目录到路径
|
||||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
@@ -20,6 +19,7 @@ if unilabos_dir not in sys.path:
|
|||||||
|
|
||||||
from unilabos.config.config import load_config, BasicConfig
|
from unilabos.config.config import load_config, BasicConfig
|
||||||
from unilabos.utils.banner_print import print_status, print_unilab_banner
|
from unilabos.utils.banner_print import print_status, print_unilab_banner
|
||||||
|
from unilabos.resources.graphio import modify_to_backend_format
|
||||||
|
|
||||||
|
|
||||||
def load_config_from_file(config_path, override_labid=None):
|
def load_config_from_file(config_path, override_labid=None):
|
||||||
@@ -95,6 +95,11 @@ def parse_args():
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="启动unilab时同时报送注册表信息",
|
help="启动unilab时同时报送注册表信息",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--use_remote_resource",
|
||||||
|
action="store_true",
|
||||||
|
help="启动unilab时使用远程资源启动",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--config",
|
"--config",
|
||||||
type=str,
|
type=str,
|
||||||
@@ -129,6 +134,23 @@ def parse_args():
|
|||||||
default="",
|
default="",
|
||||||
help="实验室唯一ID,也可通过环境变量 UNILABOS_MQCONFIG_LABID 设置或传入--config设置",
|
help="实验室唯一ID,也可通过环境变量 UNILABOS_MQCONFIG_LABID 设置或传入--config设置",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--ak",
|
||||||
|
type=str,
|
||||||
|
default="",
|
||||||
|
help="实验室请求的ak",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--sk",
|
||||||
|
type=str,
|
||||||
|
default="",
|
||||||
|
help="实验室请求的sk",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--websocket",
|
||||||
|
action="store_true",
|
||||||
|
help="使用websocket而非mqtt作为通信协议",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--skip_env_check",
|
"--skip_env_check",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
@@ -162,7 +184,7 @@ def main():
|
|||||||
else:
|
else:
|
||||||
working_dir = os.path.abspath(os.path.join(os.getcwd(), "unilabos_data"))
|
working_dir = os.path.abspath(os.path.join(os.getcwd(), "unilabos_data"))
|
||||||
if args_dict.get("working_dir"):
|
if args_dict.get("working_dir"):
|
||||||
working_dir = args_dict.get("working_dir")
|
working_dir = args_dict.get("working_dir", "")
|
||||||
if config_path and not os.path.exists(config_path):
|
if config_path and not os.path.exists(config_path):
|
||||||
config_path = os.path.join(working_dir, "local_config.py")
|
config_path = os.path.join(working_dir, "local_config.py")
|
||||||
if not os.path.exists(config_path):
|
if not os.path.exists(config_path):
|
||||||
@@ -171,6 +193,8 @@ def main():
|
|||||||
"error",
|
"error",
|
||||||
)
|
)
|
||||||
os._exit(1)
|
os._exit(1)
|
||||||
|
elif config_path and os.path.exists(config_path):
|
||||||
|
working_dir = os.path.dirname(config_path)
|
||||||
elif os.path.exists(working_dir) and os.path.exists(os.path.join(working_dir, "local_config.py")):
|
elif os.path.exists(working_dir) and os.path.exists(os.path.join(working_dir, "local_config.py")):
|
||||||
config_path = os.path.join(working_dir, "local_config.py")
|
config_path = os.path.join(working_dir, "local_config.py")
|
||||||
elif not config_path and (
|
elif not config_path and (
|
||||||
@@ -193,11 +217,25 @@ def main():
|
|||||||
print_status(f"当前工作目录为 {working_dir}", "info")
|
print_status(f"当前工作目录为 {working_dir}", "info")
|
||||||
load_config_from_file(config_path, args_dict["labid"])
|
load_config_from_file(config_path, args_dict["labid"])
|
||||||
|
|
||||||
|
if args_dict["use_remote_resource"]:
|
||||||
|
print_status("使用远程资源启动", "info")
|
||||||
|
from unilabos.app.web import http_client
|
||||||
|
|
||||||
|
res = http_client.resource_get("host_node", False)
|
||||||
|
if str(res.get("code", 0)) == "0" and len(res.get("data", [])) > 0:
|
||||||
|
print_status("远程资源已存在,使用云端物料!", "info")
|
||||||
|
args_dict["graph"] = None
|
||||||
|
else:
|
||||||
|
print_status("远程资源不存在,本地将进行首次上报!", "info")
|
||||||
|
|
||||||
# 设置BasicConfig参数
|
# 设置BasicConfig参数
|
||||||
|
BasicConfig.ak = args_dict.get("ak", "")
|
||||||
|
BasicConfig.sk = args_dict.get("sk", "")
|
||||||
BasicConfig.working_dir = working_dir
|
BasicConfig.working_dir = working_dir
|
||||||
BasicConfig.is_host_mode = not args_dict.get("without_host", False)
|
BasicConfig.is_host_mode = not args_dict.get("without_host", False)
|
||||||
BasicConfig.slave_no_host = args_dict.get("slave_no_host", False)
|
BasicConfig.slave_no_host = args_dict.get("slave_no_host", False)
|
||||||
BasicConfig.upload_registry = args_dict.get("upload_registry", False)
|
BasicConfig.upload_registry = args_dict.get("upload_registry", False)
|
||||||
|
BasicConfig.communication_protocol = "websocket" if args_dict.get("websocket", False) else "mqtt"
|
||||||
machine_name = os.popen("hostname").read().strip()
|
machine_name = os.popen("hostname").read().strip()
|
||||||
machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name])
|
machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name])
|
||||||
BasicConfig.machine_name = machine_name
|
BasicConfig.machine_name = machine_name
|
||||||
@@ -210,7 +248,7 @@ def main():
|
|||||||
dict_to_nested_dict,
|
dict_to_nested_dict,
|
||||||
initialize_resources,
|
initialize_resources,
|
||||||
)
|
)
|
||||||
from unilabos.app.mq import mqtt_client
|
from unilabos.app.communication import get_communication_client
|
||||||
from unilabos.registry.registry import build_registry
|
from unilabos.registry.registry import build_registry
|
||||||
from unilabos.app.backend import start_backend
|
from unilabos.app.backend import start_backend
|
||||||
from unilabos.app.web import http_client
|
from unilabos.app.web import http_client
|
||||||
@@ -220,7 +258,7 @@ def main():
|
|||||||
print_unilab_banner(args_dict)
|
print_unilab_banner(args_dict)
|
||||||
|
|
||||||
# 注册表
|
# 注册表
|
||||||
build_registry(args_dict["registry_path"])
|
lab_registry = build_registry(args_dict["registry_path"], False, args_dict["upload_registry"])
|
||||||
if args_dict["graph"] is None:
|
if args_dict["graph"] is None:
|
||||||
request_startup_json = http_client.request_startup_json()
|
request_startup_json = http_client.request_startup_json()
|
||||||
if not request_startup_json:
|
if not request_startup_json:
|
||||||
@@ -241,6 +279,27 @@ def main():
|
|||||||
|
|
||||||
graph_res.physical_setup_graph = graph
|
graph_res.physical_setup_graph = graph
|
||||||
resource_edge_info = modify_to_backend_format(data["links"])
|
resource_edge_info = modify_to_backend_format(data["links"])
|
||||||
|
materials = lab_registry.obtain_registry_resource_info()
|
||||||
|
materials.extend(lab_registry.obtain_registry_device_info())
|
||||||
|
materials = {k["id"]: k for k in materials}
|
||||||
|
nodes = {k["id"]: k for k in data["nodes"]}
|
||||||
|
edge_info = len(resource_edge_info)
|
||||||
|
for ind, i in enumerate(resource_edge_info[::-1]):
|
||||||
|
source_node = nodes[i["source"]]
|
||||||
|
target_node = nodes[i["target"]]
|
||||||
|
source_handle = i["sourceHandle"]
|
||||||
|
target_handle = i["targetHandle"]
|
||||||
|
source_handler_keys = [h["handler_key"] for h in materials[source_node["class"]]["handles"] if h["io_type"] == 'source']
|
||||||
|
target_handler_keys = [h["handler_key"] for h in materials[target_node["class"]]["handles"] if h["io_type"] == 'target']
|
||||||
|
if not source_handle in source_handler_keys:
|
||||||
|
print_status(f"节点 {source_node['id']} 的source端点 {source_handle} 不存在,请检查,支持的端点 {source_handler_keys}", "error")
|
||||||
|
resource_edge_info.pop(edge_info - ind - 1)
|
||||||
|
continue
|
||||||
|
if not target_handle in target_handler_keys:
|
||||||
|
print_status(f"节点 {target_node['id']} 的target端点 {target_handle} 不存在,请检查,支持的端点 {target_handler_keys}", "error")
|
||||||
|
resource_edge_info.pop(edge_info - ind - 1)
|
||||||
|
continue
|
||||||
|
|
||||||
devices_and_resources = dict_from_graph(graph_res.physical_setup_graph)
|
devices_and_resources = dict_from_graph(graph_res.physical_setup_graph)
|
||||||
# args_dict["resources_config"] = initialize_resources(list(deepcopy(devices_and_resources).values()))
|
# args_dict["resources_config"] = initialize_resources(list(deepcopy(devices_and_resources).values()))
|
||||||
args_dict["resources_config"] = list(devices_and_resources.values())
|
args_dict["resources_config"] = list(devices_and_resources.values())
|
||||||
@@ -258,19 +317,22 @@ def main():
|
|||||||
|
|
||||||
args_dict["bridges"] = []
|
args_dict["bridges"] = []
|
||||||
|
|
||||||
|
# 获取通信客户端(根据配置选择MQTT或WebSocket)
|
||||||
|
comm_client = get_communication_client()
|
||||||
|
|
||||||
if "mqtt" in args_dict["app_bridges"]:
|
if "mqtt" in args_dict["app_bridges"]:
|
||||||
args_dict["bridges"].append(mqtt_client)
|
args_dict["bridges"].append(comm_client)
|
||||||
if "fastapi" in args_dict["app_bridges"]:
|
if "fastapi" in args_dict["app_bridges"]:
|
||||||
args_dict["bridges"].append(http_client)
|
args_dict["bridges"].append(http_client)
|
||||||
if "mqtt" in args_dict["app_bridges"]:
|
if "mqtt" in args_dict["app_bridges"]:
|
||||||
|
|
||||||
def _exit(signum, frame):
|
def _exit(signum, frame):
|
||||||
mqtt_client.stop()
|
comm_client.stop()
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, _exit)
|
signal.signal(signal.SIGINT, _exit)
|
||||||
signal.signal(signal.SIGTERM, _exit)
|
signal.signal(signal.SIGTERM, _exit)
|
||||||
mqtt_client.start()
|
comm_client.start()
|
||||||
args_dict["resources_mesh_config"] = {}
|
args_dict["resources_mesh_config"] = {}
|
||||||
args_dict["resources_edge_config"] = resource_edge_info
|
args_dict["resources_edge_config"] = resource_edge_info
|
||||||
# web visiualize 2D
|
# web visiualize 2D
|
||||||
|
|||||||
@@ -50,11 +50,16 @@ class Resp(BaseModel):
|
|||||||
|
|
||||||
class JobAddReq(BaseModel):
|
class JobAddReq(BaseModel):
|
||||||
device_id: str = Field(examples=["Gripper"], description="device id")
|
device_id: str = Field(examples=["Gripper"], description="device id")
|
||||||
data: dict = Field(examples=[{"position": 30, "torque": 5, "action": "push_to"}])
|
action: str = Field(examples=["_execute_driver_command_async"], description="action name", default="")
|
||||||
|
action_type: str = Field(examples=["unilabos_msgs.action._str_single_input.StrSingleInput"], description="action name", default="")
|
||||||
|
action_args: dict = Field(examples=[{'string': 'string'}], description="action name", default="")
|
||||||
|
task_id: str = Field(examples=["task_id"], description="task uuid")
|
||||||
job_id: str = Field(examples=["job_id"], description="goal uuid")
|
job_id: str = Field(examples=["job_id"], description="goal uuid")
|
||||||
node_id: str = Field(examples=["node_id"], description="node uuid")
|
node_id: str = Field(examples=["node_id"], description="node uuid")
|
||||||
server_info: dict = Field(examples=[{"send_timestamp": 1717000000.0}], description="server info")
|
server_info: dict = Field(examples=[{"send_timestamp": 1717000000.0}], description="server info")
|
||||||
|
|
||||||
|
data: dict = Field(examples=[{"position": 30, "torque": 5, "action": "push_to"}], default={})
|
||||||
|
|
||||||
|
|
||||||
class JobStepFinishReq(BaseModel):
|
class JobStepFinishReq(BaseModel):
|
||||||
token: str = Field(examples=["030944"], description="token")
|
token: str = Field(examples=["030944"], description="token")
|
||||||
|
|||||||
@@ -15,17 +15,20 @@ import os
|
|||||||
from unilabos.config.config import MQConfig
|
from unilabos.config.config import MQConfig
|
||||||
from unilabos.app.controler import job_add
|
from unilabos.app.controler import job_add
|
||||||
from unilabos.app.model import JobAddReq
|
from unilabos.app.model import JobAddReq
|
||||||
|
from unilabos.app.communication import BaseCommunicationClient
|
||||||
from unilabos.utils import logger
|
from unilabos.utils import logger
|
||||||
from unilabos.utils.type_check import TypeEncoder
|
from unilabos.utils.type_check import TypeEncoder
|
||||||
|
|
||||||
from paho.mqtt.enums import CallbackAPIVersion
|
from paho.mqtt.enums import CallbackAPIVersion
|
||||||
|
|
||||||
|
|
||||||
class MQTTClient:
|
class MQTTClient(BaseCommunicationClient):
|
||||||
mqtt_disable = True
|
mqtt_disable = True
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
self.mqtt_disable = not MQConfig.lab_id
|
self.mqtt_disable = not MQConfig.lab_id
|
||||||
|
self.is_disabled = self.mqtt_disable # 更新父类属性
|
||||||
self.client_id = f"{MQConfig.group_id}@@@{MQConfig.lab_id}{uuid.uuid4()}"
|
self.client_id = f"{MQConfig.group_id}@@@{MQConfig.lab_id}{uuid.uuid4()}"
|
||||||
logger.info("[MQTT] Client_id: " + self.client_id)
|
logger.info("[MQTT] Client_id: " + self.client_id)
|
||||||
self.client = mqtt.Client(CallbackAPIVersion.VERSION2, client_id=self.client_id, protocol=mqtt.MQTTv5)
|
self.client = mqtt.Client(CallbackAPIVersion.VERSION2, client_id=self.client_id, protocol=mqtt.MQTTv5)
|
||||||
@@ -166,7 +169,7 @@ class MQTTClient:
|
|||||||
status = {"data": device_status.get(device_id, {}), "device_id": device_id, "timestamp": time.time()}
|
status = {"data": device_status.get(device_id, {}), "device_id": device_id, "timestamp": time.time()}
|
||||||
address = f"labs/{MQConfig.lab_id}/devices/"
|
address = f"labs/{MQConfig.lab_id}/devices/"
|
||||||
self.client.publish(address, json.dumps(status), qos=2)
|
self.client.publish(address, json.dumps(status), qos=2)
|
||||||
logger.info(f"Device {device_id} status published: address: {address}, {status}")
|
# logger.info(f"Device {device_id} status published: address: {address}, {status}")
|
||||||
|
|
||||||
def publish_job_status(self, feedback_data: dict, job_id: str, status: str, return_info: Optional[str] = None):
|
def publish_job_status(self, feedback_data: dict, job_id: str, status: str, return_info: Optional[str] = None):
|
||||||
if self.mqtt_disable:
|
if self.mqtt_disable:
|
||||||
@@ -208,11 +211,12 @@ class MQTTClient:
|
|||||||
self.client.subscribe(pong_topic, 0)
|
self.client.subscribe(pong_topic, 0)
|
||||||
logger.debug(f"Subscribed to pong topic: {pong_topic}")
|
logger.debug(f"Subscribed to pong topic: {pong_topic}")
|
||||||
|
|
||||||
def handle_pong(self, pong_data: dict):
|
@property
|
||||||
"""处理pong响应(这个方法会在收到pong消息时被调用)"""
|
def is_connected(self) -> bool:
|
||||||
logger.debug(f"Pong received: {pong_data}")
|
"""检查MQTT是否已连接"""
|
||||||
# 这里会被HostNode的ping-pong处理逻辑调用
|
if self.is_disabled:
|
||||||
pass
|
return False
|
||||||
|
return hasattr(self.client, "is_connected") and self.client.is_connected()
|
||||||
|
|
||||||
|
|
||||||
mqtt_client = MQTTClient()
|
mqtt_client = MQTTClient()
|
||||||
|
|||||||
@@ -1,44 +1,70 @@
|
|||||||
import argparse
|
import argparse
|
||||||
|
import json
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from unilabos.config.config import BasicConfig
|
||||||
from unilabos.registry.registry import build_registry
|
from unilabos.registry.registry import build_registry
|
||||||
|
|
||||||
from unilabos.app.main import load_config_from_file
|
from unilabos.app.main import load_config_from_file
|
||||||
from unilabos.utils.log import logger
|
from unilabos.utils.log import logger
|
||||||
|
from unilabos.utils.type_check import TypeEncoder
|
||||||
|
|
||||||
|
|
||||||
def register_devices_and_resources(mqtt_client, lab_registry):
|
def register_devices_and_resources(comm_client, lab_registry):
|
||||||
"""
|
"""
|
||||||
注册设备和资源到 MQTT
|
注册设备和资源到通信服务器(MQTT/WebSocket)
|
||||||
"""
|
"""
|
||||||
logger.info("[UniLab Register] 开始注册设备和资源...")
|
|
||||||
|
|
||||||
# 注册设备信息
|
|
||||||
for device_info in lab_registry.obtain_registry_device_info():
|
|
||||||
mqtt_client.publish_registry(device_info["id"], device_info, False)
|
|
||||||
logger.debug(f"[UniLab Register] 注册设备: {device_info['id']}")
|
|
||||||
|
|
||||||
# # 注册资源信息
|
|
||||||
# for resource_info in lab_registry.obtain_registry_resource_info():
|
|
||||||
# mqtt_client.publish_registry(resource_info["id"], resource_info, False)
|
|
||||||
# logger.debug(f"[UniLab Register] 注册资源: {resource_info['id']}")
|
|
||||||
|
|
||||||
# 注册资源信息 - 使用HTTP方式
|
# 注册资源信息 - 使用HTTP方式
|
||||||
from unilabos.app.web.client import http_client
|
from unilabos.app.web.client import http_client
|
||||||
|
|
||||||
resources_to_register = {}
|
logger.info("[UniLab Register] 开始注册设备和资源...")
|
||||||
for resource_info in lab_registry.obtain_registry_resource_info():
|
if BasicConfig.auth_secret():
|
||||||
resources_to_register[resource_info["id"]] = resource_info
|
# 注册设备信息
|
||||||
logger.debug(f"[UniLab Register] 准备注册资源: {resource_info['id']}")
|
devices_to_register = {}
|
||||||
|
for device_info in lab_registry.obtain_registry_device_info():
|
||||||
|
devices_to_register[device_info["id"]] = json.loads(
|
||||||
|
json.dumps(device_info, ensure_ascii=False, cls=TypeEncoder)
|
||||||
|
)
|
||||||
|
logger.debug(f"[UniLab Register] 收集设备: {device_info['id']}")
|
||||||
|
resources_to_register = {}
|
||||||
|
for resource_info in lab_registry.obtain_registry_resource_info():
|
||||||
|
resources_to_register[resource_info["id"]] = resource_info
|
||||||
|
logger.debug(f"[UniLab Register] 收集资源: {resource_info['id']}")
|
||||||
|
print(
|
||||||
|
"[UniLab Register] 设备注册",
|
||||||
|
http_client.resource_registry({"resources": list(devices_to_register.values())}).text,
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
"[UniLab Register] 资源注册",
|
||||||
|
http_client.resource_registry({"resources": list(resources_to_register.values())}).text,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 注册设备信息
|
||||||
|
for device_info in lab_registry.obtain_registry_device_info():
|
||||||
|
comm_client.publish_registry(device_info["id"], device_info, False)
|
||||||
|
logger.debug(f"[UniLab Register] 注册设备: {device_info['id']}")
|
||||||
|
|
||||||
if resources_to_register:
|
# # 注册资源信息
|
||||||
start_time = time.time()
|
# for resource_info in lab_registry.obtain_registry_resource_info():
|
||||||
response = http_client.resource_registry(resources_to_register)
|
# comm_client.publish_registry(resource_info["id"], resource_info, False)
|
||||||
cost_time = time.time() - start_time
|
# logger.debug(f"[UniLab Register] 注册资源: {resource_info['id']}")
|
||||||
if response.status_code in [200, 201]:
|
|
||||||
logger.info(f"[UniLab Register] 成功通过HTTP注册 {len(resources_to_register)} 个资源 {cost_time}ms")
|
resources_to_register = {}
|
||||||
else:
|
for resource_info in lab_registry.obtain_registry_resource_info():
|
||||||
logger.error(f"[UniLab Register] HTTP注册资源失败: {response.status_code}, {response.text} {cost_time}ms")
|
resources_to_register[resource_info["id"]] = resource_info
|
||||||
|
logger.debug(f"[UniLab Register] 准备注册资源: {resource_info['id']}")
|
||||||
|
|
||||||
|
if resources_to_register:
|
||||||
|
start_time = time.time()
|
||||||
|
response = http_client.resource_registry(resources_to_register)
|
||||||
|
cost_time = time.time() - start_time
|
||||||
|
if response.status_code in [200, 201]:
|
||||||
|
logger.info(f"[UniLab Register] 成功通过HTTP注册 {len(resources_to_register)} 个资源 {cost_time}ms")
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
f"[UniLab Register] HTTP注册资源失败: {response.status_code}, {response.text} {cost_time}ms"
|
||||||
|
)
|
||||||
logger.info("[UniLab Register] 设备和资源注册完成.")
|
logger.info("[UniLab Register] 设备和资源注册完成.")
|
||||||
|
|
||||||
|
|
||||||
@@ -60,6 +86,18 @@ def main():
|
|||||||
default=None,
|
default=None,
|
||||||
help="配置文件路径,支持.py格式的Python配置文件",
|
help="配置文件路径,支持.py格式的Python配置文件",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--ak",
|
||||||
|
type=str,
|
||||||
|
default="",
|
||||||
|
help="实验室请求的ak",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--sk",
|
||||||
|
type=str,
|
||||||
|
default="",
|
||||||
|
help="实验室请求的sk",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--complete_registry",
|
"--complete_registry",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
@@ -68,17 +106,20 @@ def main():
|
|||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
load_config_from_file(args.config)
|
load_config_from_file(args.config)
|
||||||
|
BasicConfig.ak = args.ak
|
||||||
|
BasicConfig.sk = args.sk
|
||||||
# 构建注册表
|
# 构建注册表
|
||||||
build_registry(args.registry, args.complete_registry)
|
build_registry(args.registry, args.complete_registry, True)
|
||||||
from unilabos.app.mq import mqtt_client
|
from unilabos.app.communication import get_communication_client
|
||||||
|
|
||||||
# 连接mqtt
|
# 获取通信客户端并启动连接
|
||||||
mqtt_client.start()
|
comm_client = get_communication_client()
|
||||||
|
comm_client.start()
|
||||||
|
|
||||||
from unilabos.registry.registry import lab_registry
|
from unilabos.registry.registry import lab_registry
|
||||||
|
|
||||||
# 注册设备和资源
|
# 注册设备和资源
|
||||||
register_devices_and_resources(mqtt_client, lab_registry)
|
register_devices_and_resources(comm_client, lab_registry)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,7 @@ from unilabos.utils import logger
|
|||||||
|
|
||||||
class HTTPClient:
|
class HTTPClient:
|
||||||
"""HTTP客户端,用于与远程服务器通信"""
|
"""HTTP客户端,用于与远程服务器通信"""
|
||||||
|
backend_go = False # 是否使用Go后端
|
||||||
|
|
||||||
def __init__(self, remote_addr: Optional[str] = None, auth: Optional[str] = None) -> None:
|
def __init__(self, remote_addr: Optional[str] = None, auth: Optional[str] = None) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -28,7 +29,13 @@ class HTTPClient:
|
|||||||
if auth is not None:
|
if auth is not None:
|
||||||
self.auth = auth
|
self.auth = auth
|
||||||
else:
|
else:
|
||||||
self.auth = MQConfig.lab_id
|
auth_secret = BasicConfig.auth_secret()
|
||||||
|
if auth_secret:
|
||||||
|
self.auth = auth_secret
|
||||||
|
self.backend_go = True
|
||||||
|
info(f"正在使用ak sk作为授权信息 {auth_secret}")
|
||||||
|
else:
|
||||||
|
self.auth = MQConfig.lab_id
|
||||||
info(f"HTTPClient 初始化完成: remote_addr={self.remote_addr}")
|
info(f"HTTPClient 初始化完成: remote_addr={self.remote_addr}")
|
||||||
|
|
||||||
def resource_edge_add(self, resources: List[Dict[str, Any]], database_process_later: bool) -> requests.Response:
|
def resource_edge_add(self, resources: List[Dict[str, Any]], database_process_later: bool) -> requests.Response:
|
||||||
@@ -43,11 +50,18 @@ class HTTPClient:
|
|||||||
"""
|
"""
|
||||||
database_param = 1 if database_process_later else 0
|
database_param = 1 if database_process_later else 0
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{self.remote_addr}/lab/resource/edge/batch_create/?database_process_later={database_param}",
|
f"{self.remote_addr}/lab/resource/edge/batch_create/?database_process_later={database_param}"
|
||||||
json=resources,
|
if not self.backend_go else f"{self.remote_addr}/lab/material/edge",
|
||||||
headers={"Authorization": f"lab {self.auth}"},
|
json={
|
||||||
|
"edges": resources,
|
||||||
|
} if self.backend_go else resources,
|
||||||
|
headers={"Authorization": f"{'lab' if not self.backend_go else 'Lab'} {self.auth}"},
|
||||||
timeout=100,
|
timeout=100,
|
||||||
)
|
)
|
||||||
|
if self.backend_go and response.status_code == 200:
|
||||||
|
res = response.json()
|
||||||
|
if "code" in res and res["code"] != 0:
|
||||||
|
logger.error(f"添加物料关系失败: {response.text}")
|
||||||
if response.status_code != 200 and response.status_code != 201:
|
if response.status_code != 200 and response.status_code != 201:
|
||||||
logger.error(f"添加物料关系失败: {response.status_code}, {response.text}")
|
logger.error(f"添加物料关系失败: {response.status_code}, {response.text}")
|
||||||
return response
|
return response
|
||||||
@@ -63,11 +77,15 @@ class HTTPClient:
|
|||||||
Response: API响应对象
|
Response: API响应对象
|
||||||
"""
|
"""
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{self.remote_addr}/lab/resource/?database_process_later={1 if database_process_later else 0}",
|
f"{self.remote_addr}/lab/resource/?database_process_later={1 if database_process_later else 0}" if not self.backend_go else f"{self.remote_addr}/lab/material",
|
||||||
json=resources,
|
json=resources if not self.backend_go else {"nodes": resources},
|
||||||
headers={"Authorization": f"lab {self.auth}"},
|
headers={"Authorization": f"{'lab' if not self.backend_go else 'Lab'} {self.auth}"},
|
||||||
timeout=100,
|
timeout=100,
|
||||||
)
|
)
|
||||||
|
if self.backend_go and response.status_code == 200:
|
||||||
|
res = response.json()
|
||||||
|
if "code" in res and res["code"] != 0:
|
||||||
|
logger.error(f"添加物料失败: {response.text}")
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
logger.error(f"添加物料失败: {response.text}")
|
logger.error(f"添加物料失败: {response.text}")
|
||||||
return response
|
return response
|
||||||
@@ -84,9 +102,9 @@ class HTTPClient:
|
|||||||
Dict: 返回的资源数据
|
Dict: 返回的资源数据
|
||||||
"""
|
"""
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
f"{self.remote_addr}/lab/resource/?edge_format=1",
|
f"{self.remote_addr}/lab/resource/?edge_format=1" if not self.backend_go else f"{self.remote_addr}/lab/material",
|
||||||
params={"id": id, "with_children": with_children},
|
params={"id": id, "with_children": with_children},
|
||||||
headers={"Authorization": f"lab {self.auth}"},
|
headers={"Authorization": f"{'lab' if not self.backend_go else 'Lab'} {self.auth}"},
|
||||||
timeout=20,
|
timeout=20,
|
||||||
)
|
)
|
||||||
return response.json()
|
return response.json()
|
||||||
@@ -104,7 +122,7 @@ class HTTPClient:
|
|||||||
response = requests.delete(
|
response = requests.delete(
|
||||||
f"{self.remote_addr}/lab/resource/batch_delete/",
|
f"{self.remote_addr}/lab/resource/batch_delete/",
|
||||||
params={"id": id},
|
params={"id": id},
|
||||||
headers={"Authorization": f"lab {self.auth}"},
|
headers={"Authorization": f"{'lab' if not self.backend_go else 'Lab'} {self.auth}"},
|
||||||
timeout=20,
|
timeout=20,
|
||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
@@ -122,7 +140,7 @@ class HTTPClient:
|
|||||||
response = requests.patch(
|
response = requests.patch(
|
||||||
f"{self.remote_addr}/lab/resource/batch_update/?edge_format=1",
|
f"{self.remote_addr}/lab/resource/batch_update/?edge_format=1",
|
||||||
json=resources,
|
json=resources,
|
||||||
headers={"Authorization": f"lab {self.auth}"},
|
headers={"Authorization": f"{'lab' if not self.backend_go else 'Lab'} {self.auth}"},
|
||||||
timeout=100,
|
timeout=100,
|
||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
@@ -146,25 +164,25 @@ class HTTPClient:
|
|||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{self.remote_addr}/api/account/file_upload/{scene}",
|
f"{self.remote_addr}/api/account/file_upload/{scene}",
|
||||||
files=files,
|
files=files,
|
||||||
headers={"Authorization": f"lab {self.auth}"},
|
headers={"Authorization": f"{'lab' if not self.backend_go else 'Lab'} {self.auth}"},
|
||||||
timeout=30, # 上传文件可能需要更长的超时时间
|
timeout=30, # 上传文件可能需要更长的超时时间
|
||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def resource_registry(self, registry_data: Dict[str, Any]) -> requests.Response:
|
def resource_registry(self, registry_data: Dict[str, Any] | List[Dict[str, Any]]) -> requests.Response:
|
||||||
"""
|
"""
|
||||||
注册资源到服务器
|
注册资源到服务器
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
registry_data: 注册表数据,格式为 {resource_id: resource_info}
|
registry_data: 注册表数据,格式为 {resource_id: resource_info} / [{resource_info}]
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Response: API响应对象
|
Response: API响应对象
|
||||||
"""
|
"""
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{self.remote_addr}/lab/registry/",
|
f"{self.remote_addr}/lab/registry/" if not self.backend_go else f"{self.remote_addr}/lab/resource",
|
||||||
json=registry_data,
|
json=registry_data,
|
||||||
headers={"Authorization": f"lab {self.auth}"},
|
headers={"Authorization": f"{'lab' if not self.backend_go else 'Lab'} {self.auth}"},
|
||||||
timeout=30,
|
timeout=30,
|
||||||
)
|
)
|
||||||
if response.status_code not in [200, 201]:
|
if response.status_code not in [200, 201]:
|
||||||
@@ -183,7 +201,7 @@ class HTTPClient:
|
|||||||
"""
|
"""
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
f"{self.remote_addr}/lab/resource/graph_info/",
|
f"{self.remote_addr}/lab/resource/graph_info/",
|
||||||
headers={"Authorization": f"lab {self.auth}"},
|
headers={"Authorization": f"{'lab' if not self.backend_go else 'Lab'} {self.auth}"},
|
||||||
timeout=(3, 30),
|
timeout=(3, 30),
|
||||||
)
|
)
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
|
|||||||
@@ -78,21 +78,23 @@ def setup_web_pages(router: APIRouter) -> None:
|
|||||||
HTMLResponse: 渲染后的HTML页面
|
HTMLResponse: 渲染后的HTML页面
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 准备设备数据
|
# 准备初始数据结构(这些数据将通过WebSocket实时更新)
|
||||||
devices = []
|
devices = []
|
||||||
resources = []
|
resources = []
|
||||||
modules = {"names": [], "classes": [], "displayed_count": 0, "total_count": 0}
|
modules = {"names": [], "classes": [], "displayed_count": 0, "total_count": 0}
|
||||||
|
|
||||||
# 获取在线设备信息
|
# 获取在线设备信息(用于初始渲染)
|
||||||
ros_node_info = get_ros_node_info()
|
ros_node_info = get_ros_node_info()
|
||||||
# 获取主机节点信息
|
# 获取主机节点信息(用于初始渲染)
|
||||||
host_node_info = get_host_node_info()
|
host_node_info = get_host_node_info()
|
||||||
# 获取Registry路径信息
|
# 获取Registry路径信息(静态信息,不需要实时更新)
|
||||||
registry_info = get_registry_info()
|
registry_info = get_registry_info()
|
||||||
|
|
||||||
# 获取已加载的设备
|
# 获取初始数据用于页面渲染(后续将被WebSocket数据覆盖)
|
||||||
if lab_registry:
|
if lab_registry:
|
||||||
devices = json.loads(json.dumps(lab_registry.obtain_registry_device_info(), ensure_ascii=False, cls=TypeEncoder))
|
devices = json.loads(
|
||||||
|
json.dumps(lab_registry.obtain_registry_device_info(), ensure_ascii=False, cls=TypeEncoder)
|
||||||
|
)
|
||||||
# 资源类型
|
# 资源类型
|
||||||
for resource_id, resource_info in lab_registry.resource_type_registry.items():
|
for resource_id, resource_info in lab_registry.resource_type_registry.items():
|
||||||
resources.append(
|
resources.append(
|
||||||
@@ -103,7 +105,7 @@ def setup_web_pages(router: APIRouter) -> None:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# 获取导入的模块
|
# 获取导入的模块(初始数据)
|
||||||
if msg_converter_manager:
|
if msg_converter_manager:
|
||||||
modules["names"] = msg_converter_manager.list_modules()
|
modules["names"] = msg_converter_manager.list_modules()
|
||||||
all_classes = [i for i in msg_converter_manager.list_classes() if "." in i]
|
all_classes = [i for i in msg_converter_manager.list_classes() if "." in i]
|
||||||
@@ -171,3 +173,20 @@ def setup_web_pages(router: APIRouter) -> None:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
error(f"打开文件夹时出错: {str(e)}")
|
error(f"打开文件夹时出错: {str(e)}")
|
||||||
return {"status": "error", "message": f"Failed to open folder: {str(e)}"}
|
return {"status": "error", "message": f"Failed to open folder: {str(e)}"}
|
||||||
|
|
||||||
|
@router.get("/registry-editor", response_class=HTMLResponse, summary="Registry Editor")
|
||||||
|
async def registry_editor_page() -> str:
|
||||||
|
"""
|
||||||
|
注册表编辑页面,用于导入Python文件并生成注册表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HTMLResponse: 渲染后的HTML页面
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 使用模板渲染页面
|
||||||
|
template = env.get_template("registry_editor.html")
|
||||||
|
html = template.render()
|
||||||
|
return html
|
||||||
|
except Exception as e:
|
||||||
|
error(f"生成注册表编辑页面时出错: {str(e)}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Error generating registry editor page: {str(e)}")
|
||||||
|
|||||||
@@ -162,7 +162,6 @@
|
|||||||
<body>
|
<body>
|
||||||
<h1>{% block header %}UniLab{% endblock %}</h1>
|
<h1>{% block header %}UniLab{% endblock %}</h1>
|
||||||
{% block nav %}
|
{% block nav %}
|
||||||
<a href="/unilabos/webtic" class="home-link">Home</a>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block top_info %}{% endblock %}
|
{% block top_info %}{% endblock %}
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %} {% block title %}UniLab API{% endblock %} {% block
|
||||||
|
header %}UniLab API{% endblock %} {% block nav %}
|
||||||
{% block title %}UniLab API{% endblock %}
|
<div class="nav-tabs">
|
||||||
|
<a
|
||||||
{% block header %}UniLab API{% endblock %}
|
href="/"
|
||||||
|
class="nav-tab"
|
||||||
{% block nav %}
|
style="background-color: #2196f3; color: white"
|
||||||
<a href="/status" class="status-link">System Status</a>
|
target="_blank"
|
||||||
{% endblock %}
|
>主页</a
|
||||||
|
>
|
||||||
{% block content %}
|
<a href="/status" class="nav-tab">状态</a>
|
||||||
<div class="card">
|
<a href="/registry-editor" class="nav-tab" target="_blank">注册表编辑</a>
|
||||||
<h2>Available Endpoints</h2>
|
|
||||||
{% for route in routes %}
|
|
||||||
<div class="endpoint">
|
|
||||||
<span class="method">{{ route.method }}</span>
|
|
||||||
<a href="{{ route.path }}">{{ route.path }}</a>
|
|
||||||
<p>{{ route.summary }}</p>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %} {% block content %}
|
||||||
|
<div class="card">
|
||||||
|
<h2>Available Endpoints</h2>
|
||||||
|
{% for route in routes %}
|
||||||
|
<div class="endpoint">
|
||||||
|
<span class="method">{{ route.method }}</span>
|
||||||
|
<a href="{{ route.path }}">{{ route.path }}</a>
|
||||||
|
<p>{{ route.summary }}</p>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
1411
unilabos/app/web/templates/registry_editor.html
Normal file
1411
unilabos/app/web/templates/registry_editor.html
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
901
unilabos/app/ws_client.py
Normal file
901
unilabos/app/ws_client.py
Normal file
@@ -0,0 +1,901 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# coding=utf-8
|
||||||
|
"""
|
||||||
|
WebSocket通信客户端和任务调度器
|
||||||
|
|
||||||
|
基于WebSocket协议的通信客户端实现,继承自BaseCommunicationClient。
|
||||||
|
包含WebSocketClient(连接管理)和TaskScheduler(任务调度)两个类。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
import threading
|
||||||
|
import asyncio
|
||||||
|
import traceback
|
||||||
|
import websockets
|
||||||
|
import ssl as ssl_module
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
from unilabos.app.model import JobAddReq
|
||||||
|
from unilabos.ros.nodes.presets.host_node import HostNode
|
||||||
|
from unilabos.utils.type_check import serialize_result_info
|
||||||
|
from unilabos.app.communication import BaseCommunicationClient
|
||||||
|
from unilabos.config.config import WSConfig, HTTPConfig, BasicConfig
|
||||||
|
from unilabos.utils import logger
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class QueueItem:
|
||||||
|
"""队列项数据结构"""
|
||||||
|
|
||||||
|
task_type: str # "query_action_status" 或 "job_call_back_status"
|
||||||
|
device_id: str
|
||||||
|
action_name: str
|
||||||
|
task_id: str
|
||||||
|
job_id: str
|
||||||
|
device_action_key: str
|
||||||
|
next_run_time: float # 下次执行时间戳
|
||||||
|
retry_count: int = 0 # 重试次数
|
||||||
|
|
||||||
|
|
||||||
|
class TaskScheduler:
|
||||||
|
"""
|
||||||
|
任务调度器类
|
||||||
|
|
||||||
|
负责任务队列管理、状态跟踪、业务逻辑处理等功能。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, message_sender: "WebSocketClient"):
|
||||||
|
"""初始化任务调度器"""
|
||||||
|
self.message_sender = message_sender
|
||||||
|
|
||||||
|
# 队列管理
|
||||||
|
self.action_queue = [] # 任务队列
|
||||||
|
self.action_queue_lock = threading.Lock() # 队列锁
|
||||||
|
|
||||||
|
# 任务状态跟踪
|
||||||
|
self.active_jobs = {} # job_id -> 任务信息
|
||||||
|
self.cancel_events = {} # job_id -> asyncio.Event for cancellation
|
||||||
|
|
||||||
|
# 立即执行标记字典 - device_id+action_name -> timestamp
|
||||||
|
self.immediate_execution_flags = {} # 存储需要立即执行的设备动作组合
|
||||||
|
self.immediate_execution_lock = threading.Lock() # 立即执行标记锁
|
||||||
|
|
||||||
|
# 队列处理器
|
||||||
|
self.queue_processor_thread = None
|
||||||
|
self.queue_running = False
|
||||||
|
|
||||||
|
# 队列处理器相关方法
|
||||||
|
def start(self) -> None:
|
||||||
|
"""启动任务调度器"""
|
||||||
|
if self.queue_running:
|
||||||
|
logger.warning("[TaskScheduler] Already running")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.queue_running = True
|
||||||
|
self.queue_processor_thread = threading.Thread(
|
||||||
|
target=self._run_queue_processor, daemon=True, name="TaskScheduler"
|
||||||
|
)
|
||||||
|
self.queue_processor_thread.start()
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
"""停止任务调度器"""
|
||||||
|
self.queue_running = False
|
||||||
|
if self.queue_processor_thread and self.queue_processor_thread.is_alive():
|
||||||
|
self.queue_processor_thread.join(timeout=5)
|
||||||
|
logger.info("[TaskScheduler] Stopped")
|
||||||
|
|
||||||
|
def _run_queue_processor(self):
|
||||||
|
"""在独立线程中运行队列处理器"""
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
try:
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
loop.run_until_complete(self._action_queue_processor())
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[TaskScheduler] Queue processor thread error: {str(e)}")
|
||||||
|
finally:
|
||||||
|
if loop:
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
async def _action_queue_processor(self) -> None:
|
||||||
|
"""队列处理器 - 从队列头部取出任务处理,保持顺序,使用list避免队尾排队问题"""
|
||||||
|
logger.info("[TaskScheduler] Action queue processor started")
|
||||||
|
|
||||||
|
try:
|
||||||
|
while self.queue_running:
|
||||||
|
try:
|
||||||
|
current_time = time.time()
|
||||||
|
items_to_process = []
|
||||||
|
items_to_requeue = []
|
||||||
|
|
||||||
|
# 使用锁安全地复制队列内容
|
||||||
|
with self.action_queue_lock:
|
||||||
|
if not self.action_queue:
|
||||||
|
# 队列为空,等待一段时间
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# 复制队列内容以避免并发修改问题
|
||||||
|
items_to_process = self.action_queue.copy()
|
||||||
|
self.action_queue.clear()
|
||||||
|
|
||||||
|
if not items_to_process:
|
||||||
|
await asyncio.sleep(0.2) # 队列为空时等待
|
||||||
|
continue
|
||||||
|
|
||||||
|
with self.immediate_execution_lock:
|
||||||
|
expired_keys = [k for k, v in self.immediate_execution_flags.items() if current_time > v]
|
||||||
|
for k in expired_keys:
|
||||||
|
del self.immediate_execution_flags[k]
|
||||||
|
immediate_execution = self.immediate_execution_flags.copy()
|
||||||
|
# 处理每个任务
|
||||||
|
for item in items_to_process:
|
||||||
|
try:
|
||||||
|
# 检查是否到了执行时间,是我们本地的执行时间,按顺序填入
|
||||||
|
if current_time < item.next_run_time and item.device_action_key not in immediate_execution:
|
||||||
|
# 还没到执行时间,保留在队列中(保持原有顺序)
|
||||||
|
items_to_requeue.append(item)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 执行相应的任务
|
||||||
|
should_continue = False
|
||||||
|
if item.task_type == "query_action_status":
|
||||||
|
should_continue = asyncio.run_coroutine_threadsafe(self._process_query_status_item(item), self.message_sender.event_loop).result()
|
||||||
|
elif item.task_type == "job_call_back_status":
|
||||||
|
should_continue = asyncio.run_coroutine_threadsafe(self._process_job_callback_item(item), self.message_sender.event_loop).result()
|
||||||
|
else:
|
||||||
|
logger.warning(f"[TaskScheduler] Unknown task type: {item.task_type}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 如果需要继续,放入重新排队列表
|
||||||
|
if should_continue:
|
||||||
|
item.next_run_time = current_time + 10 # 10秒后再次执行
|
||||||
|
item.retry_count += 1
|
||||||
|
items_to_requeue.append(item)
|
||||||
|
logger.trace( # type: ignore
|
||||||
|
f"[TaskScheduler] Re-queued {item.job_id} {item.task_type} "
|
||||||
|
f"for {item.device_action_key}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.debug(
|
||||||
|
f"[TaskScheduler] Completed {item.job_id} {item.task_type} "
|
||||||
|
f"for {item.device_action_key}"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[TaskScheduler] Error processing item {item.task_type}: {str(e)}")
|
||||||
|
|
||||||
|
# 将需要重新排队的任务放回队列开头(保持原有顺序,确保优先于新任务执行)
|
||||||
|
if items_to_requeue and self.action_queue is not None:
|
||||||
|
with self.action_queue_lock:
|
||||||
|
self.action_queue = items_to_requeue + self.action_queue
|
||||||
|
|
||||||
|
await asyncio.sleep(0.1) # 短暂等待避免过度占用CPU
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[TaskScheduler] Error in queue processor: {str(e)}")
|
||||||
|
await asyncio.sleep(1) # 错误后稍等再继续
|
||||||
|
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logger.info("[TaskScheduler] Action queue processor cancelled")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[TaskScheduler] Fatal error in queue processor: {str(e)}")
|
||||||
|
finally:
|
||||||
|
logger.info("[TaskScheduler] Action queue processor stopped")
|
||||||
|
|
||||||
|
# 队列处理方法
|
||||||
|
async def _process_query_status_item(self, item: QueueItem) -> bool:
|
||||||
|
"""处理query_action_status类型的队列项,返回True表示需要继续,False表示可以停止"""
|
||||||
|
try:
|
||||||
|
# 检查设备状态
|
||||||
|
host_node = HostNode.get_instance(0)
|
||||||
|
if not host_node:
|
||||||
|
logger.error("[TaskScheduler] HostNode instance not available in queue processor")
|
||||||
|
return False
|
||||||
|
|
||||||
|
action_jobs = len(host_node._device_action_status[item.device_action_key].job_ids)
|
||||||
|
free = not bool(action_jobs)
|
||||||
|
|
||||||
|
# 发送状态报告
|
||||||
|
if free:
|
||||||
|
# 设备空闲,发送最终状态并停止
|
||||||
|
# 下面要增加和handle_query_state相同的逻辑
|
||||||
|
host_node._device_action_status[item.device_action_key].job_ids[item.job_id] = time.time()
|
||||||
|
await self._publish_device_action_state(
|
||||||
|
item.device_id, item.action_name, item.task_id, item.job_id, "query_action_status", True, 0
|
||||||
|
)
|
||||||
|
return False # 停止继续监控
|
||||||
|
else:
|
||||||
|
# 设备忙碌,发送状态并继续监控
|
||||||
|
await self._publish_device_action_state(
|
||||||
|
item.device_id, item.action_name, item.task_id, item.job_id, "query_action_status", False, 10
|
||||||
|
)
|
||||||
|
return True # 继续监控
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[TaskScheduler] Error processing query status item: {str(e)}")
|
||||||
|
return False # 出错则停止
|
||||||
|
|
||||||
|
async def _process_job_callback_item(self, item: QueueItem) -> bool:
|
||||||
|
"""处理job_call_back_status类型的队列项,返回True表示需要继续,False表示可以停止"""
|
||||||
|
try:
|
||||||
|
# 检查任务是否还在活跃列表中
|
||||||
|
if item.job_id not in self.active_jobs:
|
||||||
|
logger.debug(f"[TaskScheduler] Job {item.job_id} no longer active")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 检查是否收到取消信号
|
||||||
|
if item.job_id in self.cancel_events and self.cancel_events[item.job_id].is_set():
|
||||||
|
logger.info(f"[TaskScheduler] Job {item.job_id} cancelled via cancel event")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 检查设备状态
|
||||||
|
host_node = HostNode.get_instance(0)
|
||||||
|
if not host_node:
|
||||||
|
logger.error(
|
||||||
|
f"[TaskScheduler] HostNode instance not available in job callback queue for job_id: {item.job_id}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
action_jobs = len(host_node._device_action_status[item.device_action_key].job_ids)
|
||||||
|
free = not bool(action_jobs)
|
||||||
|
|
||||||
|
# 发送job_call_back_status状态
|
||||||
|
await self._publish_device_action_state(
|
||||||
|
item.device_id, item.action_name, item.task_id, item.job_id, "job_call_back_status", free, 10
|
||||||
|
)
|
||||||
|
|
||||||
|
# 如果任务完成,停止监控
|
||||||
|
if free:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True # 继续监控
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[TaskScheduler] Error processing job callback item for job_id {item.job_id}: {str(e)}")
|
||||||
|
return False # 出错则停止
|
||||||
|
|
||||||
|
# 消息发送方法
|
||||||
|
async def _publish_device_action_state(
|
||||||
|
self, device_id: str, action_name: str, task_id: str, job_id: str, typ: str, free: bool, need_more: int
|
||||||
|
) -> None:
|
||||||
|
"""发布设备动作状态"""
|
||||||
|
message = {
|
||||||
|
"action": "report_action_state",
|
||||||
|
"data": {
|
||||||
|
"type": typ,
|
||||||
|
"device_id": device_id,
|
||||||
|
"action_name": action_name,
|
||||||
|
"task_id": task_id,
|
||||||
|
"job_id": job_id,
|
||||||
|
"free": free,
|
||||||
|
"need_more": need_more,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await self.message_sender.send_message(message)
|
||||||
|
|
||||||
|
# 业务逻辑处理方法
|
||||||
|
async def handle_query_state(self, data: Dict[str, str]) -> None:
|
||||||
|
"""处理query_action_state消息"""
|
||||||
|
device_id = data.get("device_id", "")
|
||||||
|
if not device_id:
|
||||||
|
logger.error("[TaskScheduler] query_action_state missing device_id")
|
||||||
|
return
|
||||||
|
action_name = data.get("action_name", "")
|
||||||
|
if not action_name:
|
||||||
|
logger.error("[TaskScheduler] query_action_state missing action_name")
|
||||||
|
return
|
||||||
|
task_id = data.get("task_id", "")
|
||||||
|
if not task_id:
|
||||||
|
logger.error("[TaskScheduler] query_action_state missing task_id")
|
||||||
|
return
|
||||||
|
job_id = data.get("job_id", "")
|
||||||
|
if not job_id:
|
||||||
|
logger.error("[TaskScheduler] query_action_state missing job_id")
|
||||||
|
return
|
||||||
|
|
||||||
|
device_action_key = f"/devices/{device_id}/{action_name}"
|
||||||
|
host_node = HostNode.get_instance(0)
|
||||||
|
if not host_node:
|
||||||
|
logger.error("[TaskScheduler] HostNode instance not available")
|
||||||
|
return
|
||||||
|
|
||||||
|
action_jobs = len(host_node._device_action_status[device_action_key].job_ids)
|
||||||
|
free = not bool(action_jobs)
|
||||||
|
|
||||||
|
# 如果设备空闲,立即响应free状态
|
||||||
|
if free:
|
||||||
|
await self._publish_device_action_state(
|
||||||
|
device_id, action_name, task_id, job_id, "query_action_status", True, 0
|
||||||
|
)
|
||||||
|
logger.debug(f"[TaskScheduler] {job_id} Device {device_id}/{action_name} is free, responded immediately")
|
||||||
|
host_node = HostNode.get_instance(0)
|
||||||
|
if not host_node:
|
||||||
|
logger.error(f"[TaskScheduler] HostNode instance not available for job_id: {job_id}")
|
||||||
|
return
|
||||||
|
host_node._device_action_status[device_action_key].job_ids[job_id] = time.time()
|
||||||
|
return
|
||||||
|
|
||||||
|
# 设备忙碌时,检查是否已有相同的轮询任务
|
||||||
|
if self.action_queue is not None:
|
||||||
|
with self.action_queue_lock:
|
||||||
|
# 检查是否已存在相同job_id和task_id的轮询任务
|
||||||
|
for existing_item in self.action_queue:
|
||||||
|
if (
|
||||||
|
existing_item.task_type == "query_action_status"
|
||||||
|
and existing_item.job_id == job_id
|
||||||
|
and existing_item.task_id == task_id
|
||||||
|
and existing_item.device_action_key == device_action_key
|
||||||
|
):
|
||||||
|
logger.error(
|
||||||
|
f"[TaskScheduler] Duplicate query_action_state ignored: "
|
||||||
|
f"job_id={job_id}, task_id={task_id}, server error"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 没有重复,加入轮询队列
|
||||||
|
queue_item = QueueItem(
|
||||||
|
task_type="query_action_status",
|
||||||
|
device_id=device_id,
|
||||||
|
action_name=action_name,
|
||||||
|
task_id=task_id,
|
||||||
|
job_id=job_id,
|
||||||
|
device_action_key=device_action_key,
|
||||||
|
next_run_time=time.time() + 10, # 10秒后执行
|
||||||
|
)
|
||||||
|
self.action_queue.append(queue_item)
|
||||||
|
logger.debug(
|
||||||
|
f"[TaskScheduler] {job_id} Device {device_id}/{action_name} is busy, "
|
||||||
|
f"added to polling queue {action_jobs}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 立即发送busy状态
|
||||||
|
await self._publish_device_action_state(
|
||||||
|
device_id, action_name, task_id, job_id, "query_action_status", False, 10
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warning("[TaskScheduler] Action queue not available")
|
||||||
|
|
||||||
|
async def handle_job_start(self, data: Dict[str, Any]):
|
||||||
|
"""处理作业启动消息"""
|
||||||
|
try:
|
||||||
|
req = JobAddReq(**data)
|
||||||
|
device_action_key = f"/devices/{req.device_id}/{req.action}"
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"[TaskScheduler] Starting job with job_id: {req.job_id}, "
|
||||||
|
f"device: {req.device_id}, action: {req.action}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 添加到活跃任务
|
||||||
|
self.active_jobs[req.job_id] = {
|
||||||
|
"device_id": req.device_id,
|
||||||
|
"action_name": req.action,
|
||||||
|
"task_id": data.get("task_id", ""),
|
||||||
|
"start_time": time.time(),
|
||||||
|
"device_action_key": device_action_key,
|
||||||
|
"callback_started": False, # 标记callback是否已启动
|
||||||
|
}
|
||||||
|
|
||||||
|
# 创建取消事件,todo:要移动到query_state中
|
||||||
|
self.cancel_events[req.job_id] = asyncio.Event()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 启动callback定时发送
|
||||||
|
await self._start_job_callback(req.job_id, req.device_id, req.action, req.task_id, device_action_key)
|
||||||
|
|
||||||
|
# 创建兼容HostNode的QueueItem对象
|
||||||
|
job_queue_item = QueueItem(
|
||||||
|
task_type="job_call_back_status",
|
||||||
|
device_id=req.device_id,
|
||||||
|
action_name=req.action,
|
||||||
|
task_id=req.task_id,
|
||||||
|
job_id=req.job_id,
|
||||||
|
device_action_key=device_action_key,
|
||||||
|
next_run_time=time.time(),
|
||||||
|
)
|
||||||
|
host_node = HostNode.get_instance(0)
|
||||||
|
if not host_node:
|
||||||
|
logger.error(f"[TaskScheduler] HostNode instance not available for job_id: {req.job_id}")
|
||||||
|
return
|
||||||
|
host_node.send_goal(
|
||||||
|
job_queue_item,
|
||||||
|
action_type=req.action_type,
|
||||||
|
action_kwargs=req.action_args,
|
||||||
|
server_info=req.server_info,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[TaskScheduler] Exception during job start for job_id {req.job_id}: {str(e)}")
|
||||||
|
traceback.print_exc()
|
||||||
|
# 异常结束,先停止callback,然后发送失败状态
|
||||||
|
await self._stop_job_callback(
|
||||||
|
req.job_id, "failed", serialize_result_info(traceback.format_exc(), False, {})
|
||||||
|
)
|
||||||
|
|
||||||
|
host_node = HostNode.get_instance(0)
|
||||||
|
if host_node:
|
||||||
|
host_node._device_action_status[device_action_key].job_ids.pop(req.job_id, None)
|
||||||
|
logger.warning(f"[TaskScheduler] Cleaned up failed job from HostNode: {req.job_id}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[TaskScheduler] Error handling job start: {str(e)}")
|
||||||
|
|
||||||
|
async def handle_cancel_action(self, data: Dict[str, Any]) -> None:
|
||||||
|
"""处理取消动作请求"""
|
||||||
|
task_id = data.get("task_id")
|
||||||
|
job_id = data.get("job_id")
|
||||||
|
|
||||||
|
logger.debug(f"[TaskScheduler] Handling cancel action request - task_id: {task_id}, job_id: {job_id}")
|
||||||
|
|
||||||
|
if not task_id and not job_id:
|
||||||
|
logger.error("[TaskScheduler] cancel_action missing both task_id and job_id")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 通过job_id取消
|
||||||
|
if job_id:
|
||||||
|
logger.info(f"[TaskScheduler] Cancelling job by job_id: {job_id}")
|
||||||
|
# 设置取消事件
|
||||||
|
if job_id in self.cancel_events:
|
||||||
|
self.cancel_events[job_id].set()
|
||||||
|
logger.debug(f"[TaskScheduler] Set cancel event for job_id: {job_id}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"[TaskScheduler] Cancel event not found for job_id: {job_id}")
|
||||||
|
|
||||||
|
# 停止job callback并发送取消状态
|
||||||
|
if job_id in self.active_jobs:
|
||||||
|
logger.debug(f"[TaskScheduler] Found active job for cancellation: {job_id}")
|
||||||
|
# 调用HostNode的cancel_goal
|
||||||
|
host_node = HostNode.get_instance(0)
|
||||||
|
if host_node:
|
||||||
|
host_node.cancel_goal(job_id)
|
||||||
|
logger.info(f"[TaskScheduler] Cancelled goal in HostNode for job_id: {job_id}")
|
||||||
|
else:
|
||||||
|
logger.error(f"[TaskScheduler] HostNode not available for cancel goal: {job_id}")
|
||||||
|
|
||||||
|
# 停止callback并发送取消状态
|
||||||
|
await self._stop_job_callback(job_id, "cancelled", "Job was cancelled by user request")
|
||||||
|
logger.info(f"[TaskScheduler] Stopped job callback and sent cancel status for job_id: {job_id}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"[TaskScheduler] Job not found in active jobs for cancellation: {job_id}")
|
||||||
|
|
||||||
|
# 通过task_id取消(需要查找对应的job_id)
|
||||||
|
if task_id and not job_id:
|
||||||
|
logger.debug(f"[TaskScheduler] Cancelling jobs by task_id: {task_id}")
|
||||||
|
jobs_to_cancel = []
|
||||||
|
for jid, job_info in self.active_jobs.items():
|
||||||
|
if job_info.get("task_id") == task_id:
|
||||||
|
jobs_to_cancel.append(jid)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f"[TaskScheduler] Found {len(jobs_to_cancel)} jobs to cancel for task_id {task_id}: {jobs_to_cancel}"
|
||||||
|
)
|
||||||
|
|
||||||
|
for jid in jobs_to_cancel:
|
||||||
|
logger.debug(f"[TaskScheduler] Recursively cancelling job_id: {jid} for task_id: {task_id}")
|
||||||
|
# 递归调用自身来取消每个job
|
||||||
|
await self.handle_cancel_action({"job_id": jid})
|
||||||
|
|
||||||
|
logger.debug(f"[TaskScheduler] Completed cancel action handling - task_id: {task_id}, job_id: {job_id}")
|
||||||
|
|
||||||
|
# job管理方法
|
||||||
|
async def _start_job_callback(
|
||||||
|
self, job_id: str, device_id: str, action_name: str, task_id: str, device_action_key: str
|
||||||
|
) -> None:
|
||||||
|
"""启动job的callback定时发送"""
|
||||||
|
if job_id not in self.active_jobs:
|
||||||
|
logger.debug(f"[TaskScheduler] Job not found in active jobs when starting callback: {job_id}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 检查是否已经启动过callback
|
||||||
|
if self.active_jobs[job_id].get("callback_started", False):
|
||||||
|
logger.warning(f"[TaskScheduler] Job callback already started for job_id: {job_id}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 标记callback已启动
|
||||||
|
self.active_jobs[job_id]["callback_started"] = True
|
||||||
|
|
||||||
|
# 将job_call_back_status任务放入队列
|
||||||
|
queue_item = QueueItem(
|
||||||
|
task_type="job_call_back_status",
|
||||||
|
device_id=device_id,
|
||||||
|
action_name=action_name,
|
||||||
|
task_id=task_id,
|
||||||
|
job_id=job_id,
|
||||||
|
device_action_key=device_action_key,
|
||||||
|
next_run_time=time.time() + 10, # 10秒后开始报送
|
||||||
|
)
|
||||||
|
if self.action_queue is not None:
|
||||||
|
with self.action_queue_lock:
|
||||||
|
self.action_queue.append(queue_item)
|
||||||
|
else:
|
||||||
|
logger.debug(f"[TaskScheduler] Action queue not available for job callback: {job_id}")
|
||||||
|
|
||||||
|
async def _stop_job_callback(self, job_id: str, final_status: str, return_info: Optional[str] = None) -> None:
|
||||||
|
"""停止job的callback定时发送并发送最终结果"""
|
||||||
|
logger.info(f"[TaskScheduler] Stopping job callback for job_id: {job_id} with final status: {final_status}")
|
||||||
|
if job_id not in self.active_jobs:
|
||||||
|
logger.debug(f"[TaskScheduler] Job {job_id} not found in active jobs when stopping callback")
|
||||||
|
return
|
||||||
|
|
||||||
|
job_info = self.active_jobs[job_id]
|
||||||
|
device_id = job_info["device_id"]
|
||||||
|
action_name = job_info["action_name"]
|
||||||
|
task_id = job_info["task_id"]
|
||||||
|
device_action_key = job_info["device_action_key"]
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f"[TaskScheduler] Job {job_id} details - device: {device_id}, action: {action_name}, task: {task_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 移除活跃任务和取消事件(这会让队列处理器自动停止callback)
|
||||||
|
self.active_jobs.pop(job_id, None)
|
||||||
|
self.cancel_events.pop(job_id, None)
|
||||||
|
logger.debug(f"[TaskScheduler] Removed job {job_id} from active jobs and cancel events")
|
||||||
|
|
||||||
|
# 发送最终的callback状态
|
||||||
|
await self._publish_device_action_state(
|
||||||
|
device_id, action_name, task_id, job_id, "job_call_back_status", True, 0
|
||||||
|
)
|
||||||
|
logger.debug(f"[TaskScheduler] Completed stopping job callback for {job_id} with final status: {final_status}")
|
||||||
|
|
||||||
|
# 外部接口方法
|
||||||
|
def publish_job_status(
|
||||||
|
self, feedback_data: dict, item: "QueueItem", status: str, return_info: Optional[str] = None
|
||||||
|
) -> None:
|
||||||
|
"""发布作业状态,拦截最终结果(给HostNode调用的接口)"""
|
||||||
|
if not self.message_sender.is_connected():
|
||||||
|
logger.debug(f"[TaskScheduler] Not connected, cannot publish job status for job_id: {item.job_id}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 拦截最终结果状态
|
||||||
|
if status in ["success", "failed"]:
|
||||||
|
host_node = HostNode.get_instance(0)
|
||||||
|
if host_node:
|
||||||
|
host_node._device_action_status[item.device_action_key].job_ids.pop(item.job_id)
|
||||||
|
logger.info(f"[TaskScheduler] Intercepting final status for job_id: {item.job_id} - {status}")
|
||||||
|
# 给其他同名action至少执行一次的机会
|
||||||
|
with self.immediate_execution_lock:
|
||||||
|
self.immediate_execution_flags[item.device_action_key] = time.time() + 3
|
||||||
|
# 如果是最终状态,通过_stop_job_callback处理
|
||||||
|
asyncio.run_coroutine_threadsafe(
|
||||||
|
self._stop_job_callback(item.job_id, status, return_info), self.message_sender.event_loop
|
||||||
|
).result()
|
||||||
|
# 执行结果信息上传
|
||||||
|
message = {
|
||||||
|
"action": "job_status",
|
||||||
|
"data": {
|
||||||
|
"job_id": item.job_id,
|
||||||
|
"task_id": item.task_id,
|
||||||
|
"device_id": item.device_id,
|
||||||
|
"action_name": item.action_name,
|
||||||
|
"status": status,
|
||||||
|
"feedback_data": feedback_data,
|
||||||
|
"return_info": return_info,
|
||||||
|
"timestamp": time.time(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
asyncio.run_coroutine_threadsafe(
|
||||||
|
self.message_sender.send_message(message), self.message_sender.event_loop
|
||||||
|
).result()
|
||||||
|
|
||||||
|
logger.trace(f"[TaskScheduler] Job status published: {item.job_id} - {status}") # type: ignore
|
||||||
|
|
||||||
|
def cancel_goal(self, job_id: str) -> None:
|
||||||
|
"""取消指定的任务(给外部调用的接口)"""
|
||||||
|
logger.debug(f"[TaskScheduler] External cancel request for job_id: {job_id}")
|
||||||
|
if job_id in self.cancel_events:
|
||||||
|
logger.debug(f"[TaskScheduler] Found cancel event for job_id: {job_id}, processing cancellation")
|
||||||
|
try:
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.create_task(self.handle_cancel_action({"job_id": job_id}))
|
||||||
|
logger.debug(f"[TaskScheduler] Scheduled cancel action for job_id: {job_id}")
|
||||||
|
except RuntimeError:
|
||||||
|
asyncio.run(self.handle_cancel_action({"job_id": job_id}))
|
||||||
|
logger.debug(f"[TaskScheduler] Executed cancel action for job_id: {job_id}")
|
||||||
|
logger.debug(f"[TaskScheduler] Initiated cancel for job_id: {job_id}")
|
||||||
|
else:
|
||||||
|
logger.debug(f"[TaskScheduler] Job {job_id} not found in cancel events for cancellation")
|
||||||
|
|
||||||
|
|
||||||
|
class WebSocketClient(BaseCommunicationClient):
|
||||||
|
"""
|
||||||
|
WebSocket通信客户端类
|
||||||
|
|
||||||
|
专注于WebSocket连接管理和消息传输。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.is_disabled = False
|
||||||
|
self.client_id = f"{uuid.uuid4()}"
|
||||||
|
|
||||||
|
# WebSocket连接相关
|
||||||
|
self.websocket = None
|
||||||
|
self.connection_loop = None
|
||||||
|
self.event_loop: asyncio.AbstractEventLoop = None # type: ignore
|
||||||
|
self.connection_thread = None
|
||||||
|
self.is_running = False
|
||||||
|
self.connected = False
|
||||||
|
|
||||||
|
# 消息处理
|
||||||
|
self.message_queue = asyncio.Queue() if not self.is_disabled else None
|
||||||
|
self.reconnect_count = 0
|
||||||
|
|
||||||
|
# 消息发送锁(解决并发写入问题)- 延迟初始化
|
||||||
|
self.send_lock = None
|
||||||
|
|
||||||
|
# 任务调度器
|
||||||
|
self.task_scheduler = None
|
||||||
|
|
||||||
|
# 构建WebSocket URL
|
||||||
|
self._build_websocket_url()
|
||||||
|
|
||||||
|
logger.info(f"[WebSocket] Client_id: {self.client_id}")
|
||||||
|
|
||||||
|
# 初始化方法
|
||||||
|
def _initialize_task_scheduler(self):
|
||||||
|
"""初始化任务调度器"""
|
||||||
|
if not self.task_scheduler:
|
||||||
|
self.task_scheduler = TaskScheduler(self)
|
||||||
|
self.task_scheduler.start()
|
||||||
|
logger.info("[WebSocket] Task scheduler initialized")
|
||||||
|
|
||||||
|
def _build_websocket_url(self):
|
||||||
|
"""构建WebSocket连接URL"""
|
||||||
|
if not HTTPConfig.remote_addr:
|
||||||
|
self.websocket_url = None
|
||||||
|
return
|
||||||
|
|
||||||
|
# 解析服务器URL
|
||||||
|
parsed = urlparse(HTTPConfig.remote_addr)
|
||||||
|
|
||||||
|
# 根据SSL配置选择协议
|
||||||
|
if parsed.scheme == "https":
|
||||||
|
scheme = "wss"
|
||||||
|
else:
|
||||||
|
scheme = "ws"
|
||||||
|
if ":" in parsed.netloc and parsed.port is not None:
|
||||||
|
self.websocket_url = f"{scheme}://{parsed.hostname}:{parsed.port + 1}/api/v1/ws/schedule"
|
||||||
|
else:
|
||||||
|
self.websocket_url = f"{scheme}://{parsed.netloc}/api/v1/ws/schedule"
|
||||||
|
logger.debug(f"[WebSocket] URL: {self.websocket_url}")
|
||||||
|
|
||||||
|
# 连接管理方法
|
||||||
|
def start(self) -> None:
|
||||||
|
"""启动WebSocket连接和任务调度器"""
|
||||||
|
if self.is_disabled:
|
||||||
|
logger.warning("[WebSocket] WebSocket is disabled, skipping connection.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.websocket_url:
|
||||||
|
logger.error("[WebSocket] WebSocket URL not configured")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(f"[WebSocket] Starting connection to {self.websocket_url}")
|
||||||
|
|
||||||
|
# 初始化任务调度器
|
||||||
|
self._initialize_task_scheduler()
|
||||||
|
|
||||||
|
self.is_running = True
|
||||||
|
|
||||||
|
# 在单独线程中运行WebSocket连接
|
||||||
|
self.connection_thread = threading.Thread(target=self._run_connection, daemon=True, name="WebSocketConnection")
|
||||||
|
self.connection_thread.start()
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
"""停止WebSocket连接和任务调度器"""
|
||||||
|
if self.is_disabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("[WebSocket] Stopping connection")
|
||||||
|
self.is_running = False
|
||||||
|
self.connected = False
|
||||||
|
|
||||||
|
# 停止任务调度器
|
||||||
|
if self.task_scheduler:
|
||||||
|
self.task_scheduler.stop()
|
||||||
|
|
||||||
|
if self.event_loop and self.event_loop.is_running():
|
||||||
|
asyncio.run_coroutine_threadsafe(self._close_connection(), self.event_loop)
|
||||||
|
|
||||||
|
if self.connection_thread and self.connection_thread.is_alive():
|
||||||
|
self.connection_thread.join(timeout=5)
|
||||||
|
|
||||||
|
def _run_connection(self):
|
||||||
|
"""在独立线程中运行WebSocket连接"""
|
||||||
|
try:
|
||||||
|
# 创建新的事件循环
|
||||||
|
self.event_loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(self.event_loop)
|
||||||
|
|
||||||
|
# 在正确的事件循环中创建锁
|
||||||
|
self.send_lock = asyncio.Lock()
|
||||||
|
|
||||||
|
# 运行连接逻辑
|
||||||
|
self.event_loop.run_until_complete(self._connection_handler())
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[WebSocket] Connection thread error: {str(e)}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
finally:
|
||||||
|
if self.event_loop:
|
||||||
|
self.event_loop.close()
|
||||||
|
|
||||||
|
async def _connection_handler(self):
|
||||||
|
"""处理WebSocket连接和重连逻辑"""
|
||||||
|
while self.is_running:
|
||||||
|
try:
|
||||||
|
# 构建SSL上下文
|
||||||
|
ssl_context = None
|
||||||
|
assert self.websocket_url is not None
|
||||||
|
if self.websocket_url.startswith("wss://"):
|
||||||
|
ssl_context = ssl_module.create_default_context()
|
||||||
|
ws_logger = logging.getLogger("websockets.client")
|
||||||
|
ws_logger.setLevel(logging.INFO)
|
||||||
|
async with websockets.connect(
|
||||||
|
self.websocket_url,
|
||||||
|
ssl=ssl_context,
|
||||||
|
ping_interval=WSConfig.ping_interval,
|
||||||
|
ping_timeout=10,
|
||||||
|
additional_headers={"Authorization": f"Lab {BasicConfig.auth_secret()}"},
|
||||||
|
logger=ws_logger,
|
||||||
|
) as websocket:
|
||||||
|
self.websocket = websocket
|
||||||
|
self.connected = True
|
||||||
|
self.reconnect_count = 0
|
||||||
|
|
||||||
|
logger.info(f"[WebSocket] Connected to {self.websocket_url}")
|
||||||
|
|
||||||
|
# 处理消息
|
||||||
|
await self._message_handler()
|
||||||
|
|
||||||
|
except websockets.exceptions.ConnectionClosed:
|
||||||
|
logger.warning("[WebSocket] Connection closed")
|
||||||
|
self.connected = False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[WebSocket] Connection error: {str(e)}")
|
||||||
|
self.connected = False
|
||||||
|
finally:
|
||||||
|
# WebSocket连接结束时只需重置websocket对象
|
||||||
|
self.websocket = None
|
||||||
|
|
||||||
|
# 重连逻辑
|
||||||
|
if self.is_running and self.reconnect_count < WSConfig.max_reconnect_attempts:
|
||||||
|
self.reconnect_count += 1
|
||||||
|
logger.info(
|
||||||
|
f"[WebSocket] Reconnecting in {WSConfig.reconnect_interval}s "
|
||||||
|
f"(attempt {self.reconnect_count}/{WSConfig.max_reconnect_attempts})"
|
||||||
|
)
|
||||||
|
await asyncio.sleep(WSConfig.reconnect_interval)
|
||||||
|
elif self.reconnect_count >= WSConfig.max_reconnect_attempts:
|
||||||
|
logger.error("[WebSocket] Max reconnection attempts reached")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self.reconnect_count -= 1
|
||||||
|
|
||||||
|
async def _close_connection(self):
|
||||||
|
"""关闭WebSocket连接"""
|
||||||
|
if self.websocket:
|
||||||
|
await self.websocket.close()
|
||||||
|
self.websocket = None
|
||||||
|
|
||||||
|
# 消息处理方法
|
||||||
|
async def _message_handler(self):
|
||||||
|
"""处理接收到的消息"""
|
||||||
|
if not self.websocket:
|
||||||
|
logger.error("[WebSocket] WebSocket connection is None")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
async for message in self.websocket:
|
||||||
|
try:
|
||||||
|
data = json.loads(message)
|
||||||
|
await self._process_message(data)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
logger.error(f"[WebSocket] Invalid JSON received: {message}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[WebSocket] Error processing message: {str(e)}")
|
||||||
|
except websockets.exceptions.ConnectionClosed:
|
||||||
|
logger.info("[WebSocket] Message handler stopped - connection closed")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[WebSocket] Message handler error: {str(e)}")
|
||||||
|
|
||||||
|
async def _process_message(self, input_message: Dict[str, Any]):
|
||||||
|
"""处理收到的消息"""
|
||||||
|
message_type = input_message.get("action", "")
|
||||||
|
data = input_message.get("data", {})
|
||||||
|
|
||||||
|
if message_type == "pong":
|
||||||
|
# 处理pong响应(WebSocket层面的连接管理)
|
||||||
|
self._handle_pong_sync(data)
|
||||||
|
elif self.task_scheduler:
|
||||||
|
# 其他消息交给TaskScheduler处理
|
||||||
|
if message_type == "job_start":
|
||||||
|
await self.task_scheduler.handle_job_start(data)
|
||||||
|
elif message_type == "query_action_state":
|
||||||
|
await self.task_scheduler.handle_query_state(data)
|
||||||
|
elif message_type == "cancel_action":
|
||||||
|
await self.task_scheduler.handle_cancel_action(data)
|
||||||
|
elif message_type == "":
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
logger.debug(f"[WebSocket] Unknown message: {input_message}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"[WebSocket] Task scheduler not available for message: {message_type}")
|
||||||
|
|
||||||
|
def _handle_pong_sync(self, pong_data: Dict[str, Any]):
|
||||||
|
"""同步处理pong响应"""
|
||||||
|
host_node = HostNode.get_instance(0)
|
||||||
|
if host_node:
|
||||||
|
host_node.handle_pong_response(pong_data)
|
||||||
|
|
||||||
|
# MessageSender接口实现
|
||||||
|
async def send_message(self, message: Dict[str, Any]) -> None:
|
||||||
|
"""内部发送消息方法,使用锁确保线程安全"""
|
||||||
|
if not self.connected or not self.websocket:
|
||||||
|
logger.warning("[WebSocket] Not connected, cannot send message")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 检查锁是否已初始化(在事件循环启动后才会创建)
|
||||||
|
if not self.send_lock:
|
||||||
|
logger.warning("[WebSocket] Send lock not initialized, cannot send message safely")
|
||||||
|
return
|
||||||
|
|
||||||
|
message_str = json.dumps(message, ensure_ascii=False)
|
||||||
|
# 使用异步锁防止并发写入导致的竞态条件
|
||||||
|
async with self.send_lock:
|
||||||
|
try:
|
||||||
|
await self.websocket.send(message_str)
|
||||||
|
logger.debug(f"[WebSocket] Message sent: {message['action']}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[WebSocket] Failed to send message: {str(e)}")
|
||||||
|
|
||||||
|
def is_connected(self) -> bool:
|
||||||
|
"""检查是否已连接(TaskScheduler调用的接口)"""
|
||||||
|
return self.connected and not self.is_disabled
|
||||||
|
|
||||||
|
# 基类方法实现
|
||||||
|
def publish_device_status(self, device_status: dict, device_id: str, property_name: str) -> None:
|
||||||
|
"""发布设备状态"""
|
||||||
|
if self.is_disabled or not self.connected:
|
||||||
|
return
|
||||||
|
message = {
|
||||||
|
"action": "device_status",
|
||||||
|
"data": {
|
||||||
|
"device_id": device_id,
|
||||||
|
"data": {
|
||||||
|
"property_name": property_name,
|
||||||
|
"status": device_status.get(device_id, {}).get(property_name),
|
||||||
|
"timestamp": time.time(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
asyncio.run_coroutine_threadsafe(self.send_message(message), self.event_loop).result()
|
||||||
|
logger.debug(f"[WebSocket] Device status published: {device_id}.{property_name}")
|
||||||
|
|
||||||
|
def publish_job_status(
|
||||||
|
self, feedback_data: dict, item: "QueueItem", status: str, return_info: Optional[str] = None
|
||||||
|
) -> None:
|
||||||
|
"""发布作业状态(转发给TaskScheduler)"""
|
||||||
|
if self.task_scheduler:
|
||||||
|
self.task_scheduler.publish_job_status(feedback_data, item, status, return_info)
|
||||||
|
else:
|
||||||
|
logger.debug(f"[WebSocket] Task scheduler not available for job status: {item.job_id}")
|
||||||
|
|
||||||
|
def send_ping(self, ping_id: str, timestamp: float) -> None:
|
||||||
|
"""发送ping消息"""
|
||||||
|
if self.is_disabled or not self.connected:
|
||||||
|
logger.warning("[WebSocket] Not connected, cannot send ping")
|
||||||
|
return
|
||||||
|
message = {"action": "ping", "data": {"ping_id": ping_id, "client_timestamp": timestamp}}
|
||||||
|
asyncio.run_coroutine_threadsafe(self.send_message(message), self.event_loop).result()
|
||||||
|
logger.debug(f"[WebSocket] Ping sent: {ping_id}")
|
||||||
|
|
||||||
|
def cancel_goal(self, job_id: str) -> None:
|
||||||
|
"""取消指定的任务(转发给TaskScheduler)"""
|
||||||
|
logger.debug(f"[WebSocket] Received cancel goal request for job_id: {job_id}")
|
||||||
|
if self.task_scheduler:
|
||||||
|
self.task_scheduler.cancel_goal(job_id)
|
||||||
|
logger.debug(f"[WebSocket] Forwarded cancel goal to TaskScheduler for job_id: {job_id}")
|
||||||
|
else:
|
||||||
|
logger.debug(f"[WebSocket] Task scheduler not available for cancel goal: {job_id}")
|
||||||
@@ -15,7 +15,6 @@ from .heatchill_protocol import (
|
|||||||
generate_heat_chill_to_temp_protocol # 保留导入,但不注册为协议
|
generate_heat_chill_to_temp_protocol # 保留导入,但不注册为协议
|
||||||
)
|
)
|
||||||
from .stir_protocol import generate_stir_protocol, generate_start_stir_protocol, generate_stop_stir_protocol
|
from .stir_protocol import generate_stir_protocol, generate_start_stir_protocol, generate_stop_stir_protocol
|
||||||
from .transfer_protocol import generate_transfer_protocol
|
|
||||||
from .clean_vessel_protocol import generate_clean_vessel_protocol
|
from .clean_vessel_protocol import generate_clean_vessel_protocol
|
||||||
from .dissolve_protocol import generate_dissolve_protocol
|
from .dissolve_protocol import generate_dissolve_protocol
|
||||||
from .filter_through_protocol import generate_filter_through_protocol
|
from .filter_through_protocol import generate_filter_through_protocol
|
||||||
@@ -47,6 +46,7 @@ action_protocol_generators = {
|
|||||||
HeatChillStopProtocol: generate_heat_chill_stop_protocol,
|
HeatChillStopProtocol: generate_heat_chill_stop_protocol,
|
||||||
HydrogenateProtocol: generate_hydrogenate_protocol,
|
HydrogenateProtocol: generate_hydrogenate_protocol,
|
||||||
PumpTransferProtocol: generate_pump_protocol_with_rinsing,
|
PumpTransferProtocol: generate_pump_protocol_with_rinsing,
|
||||||
|
TransferProtocol: generate_pump_protocol,
|
||||||
RecrystallizeProtocol: generate_recrystallize_protocol,
|
RecrystallizeProtocol: generate_recrystallize_protocol,
|
||||||
ResetHandlingProtocol: generate_reset_handling_protocol,
|
ResetHandlingProtocol: generate_reset_handling_protocol,
|
||||||
RunColumnProtocol: generate_run_column_protocol,
|
RunColumnProtocol: generate_run_column_protocol,
|
||||||
@@ -54,6 +54,5 @@ action_protocol_generators = {
|
|||||||
StartStirProtocol: generate_start_stir_protocol,
|
StartStirProtocol: generate_start_stir_protocol,
|
||||||
StirProtocol: generate_stir_protocol,
|
StirProtocol: generate_stir_protocol,
|
||||||
StopStirProtocol: generate_stop_stir_protocol,
|
StopStirProtocol: generate_stop_stir_protocol,
|
||||||
TransferProtocol: generate_transfer_protocol,
|
|
||||||
WashSolidProtocol: generate_wash_solid_protocol,
|
WashSolidProtocol: generate_wash_solid_protocol,
|
||||||
}
|
}
|
||||||
@@ -1,313 +1,24 @@
|
|||||||
|
from functools import partial
|
||||||
|
|
||||||
import networkx as nx
|
import networkx as nx
|
||||||
import re
|
import re
|
||||||
import logging
|
import logging
|
||||||
from typing import List, Dict, Any, Union
|
from typing import List, Dict, Any, Union
|
||||||
|
|
||||||
|
from .utils.unit_parser import parse_volume_input, parse_mass_input, parse_time_input
|
||||||
|
from .utils.vessel_parser import get_vessel, find_solid_dispenser, find_connected_stirrer, find_reagent_vessel
|
||||||
|
from .utils.logger_util import action_log
|
||||||
from .pump_protocol import generate_pump_protocol_with_rinsing
|
from .pump_protocol import generate_pump_protocol_with_rinsing
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def debug_print(message):
|
def debug_print(message):
|
||||||
"""调试输出"""
|
"""调试输出"""
|
||||||
print(f"[ADD] {message}", flush=True)
|
|
||||||
logger.info(f"[ADD] {message}")
|
logger.info(f"[ADD] {message}")
|
||||||
|
|
||||||
def parse_volume_input(volume_input: Union[str, float]) -> float:
|
|
||||||
"""
|
|
||||||
解析体积输入,支持带单位的字符串
|
|
||||||
|
|
||||||
Args:
|
|
||||||
volume_input: 体积输入(如 "2.7 mL", "2.67 mL", "?", 10.0)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: 体积(毫升)
|
|
||||||
"""
|
|
||||||
if isinstance(volume_input, (int, float)):
|
|
||||||
debug_print(f"📏 体积输入为数值: {volume_input}")
|
|
||||||
return float(volume_input)
|
|
||||||
|
|
||||||
if not volume_input or not str(volume_input).strip():
|
|
||||||
debug_print(f"⚠️ 体积输入为空,返回0.0mL")
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
volume_str = str(volume_input).lower().strip()
|
|
||||||
debug_print(f"🔍 解析体积输入: '{volume_str}'")
|
|
||||||
|
|
||||||
# 处理未知体积
|
|
||||||
if volume_str in ['?', 'unknown', 'tbd', 'to be determined']:
|
|
||||||
default_volume = 10.0 # 默认10mL
|
|
||||||
debug_print(f"❓ 检测到未知体积,使用默认值: {default_volume}mL 🎯")
|
|
||||||
return default_volume
|
|
||||||
|
|
||||||
# 移除空格并提取数字和单位
|
|
||||||
volume_clean = re.sub(r'\s+', '', volume_str)
|
|
||||||
|
|
||||||
# 匹配数字和单位的正则表达式
|
|
||||||
match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean)
|
|
||||||
|
|
||||||
if not match:
|
|
||||||
debug_print(f"❌ 无法解析体积: '{volume_str}',使用默认值10mL")
|
|
||||||
return 10.0
|
|
||||||
|
|
||||||
value = float(match.group(1))
|
|
||||||
unit = match.group(2) or 'ml' # 默认单位为毫升
|
|
||||||
|
|
||||||
# 转换为毫升
|
|
||||||
if unit in ['l', 'liter']:
|
|
||||||
volume = value * 1000.0 # L -> mL
|
|
||||||
debug_print(f"🔄 体积转换: {value}L → {volume}mL")
|
|
||||||
elif unit in ['μl', 'ul', 'microliter']:
|
|
||||||
volume = value / 1000.0 # μL -> mL
|
|
||||||
debug_print(f"🔄 体积转换: {value}μL → {volume}mL")
|
|
||||||
else: # ml, milliliter 或默认
|
|
||||||
volume = value # 已经是mL
|
|
||||||
debug_print(f"✅ 体积已为mL: {volume}mL")
|
|
||||||
|
|
||||||
return volume
|
|
||||||
|
|
||||||
def parse_mass_input(mass_input: Union[str, float]) -> float:
|
|
||||||
"""
|
|
||||||
解析质量输入,支持带单位的字符串
|
|
||||||
|
|
||||||
Args:
|
|
||||||
mass_input: 质量输入(如 "19.3 g", "4.5 g", 2.5)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: 质量(克)
|
|
||||||
"""
|
|
||||||
if isinstance(mass_input, (int, float)):
|
|
||||||
debug_print(f"⚖️ 质量输入为数值: {mass_input}g")
|
|
||||||
return float(mass_input)
|
|
||||||
|
|
||||||
if not mass_input or not str(mass_input).strip():
|
|
||||||
debug_print(f"⚠️ 质量输入为空,返回0.0g")
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
mass_str = str(mass_input).lower().strip()
|
|
||||||
debug_print(f"🔍 解析质量输入: '{mass_str}'")
|
|
||||||
|
|
||||||
# 移除空格并提取数字和单位
|
|
||||||
mass_clean = re.sub(r'\s+', '', mass_str)
|
|
||||||
|
|
||||||
# 匹配数字和单位的正则表达式
|
|
||||||
match = re.match(r'([0-9]*\.?[0-9]+)\s*(g|mg|kg|gram|milligram|kilogram)?', mass_clean)
|
|
||||||
|
|
||||||
if not match:
|
|
||||||
debug_print(f"❌ 无法解析质量: '{mass_str}',返回0.0g")
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
value = float(match.group(1))
|
|
||||||
unit = match.group(2) or 'g' # 默认单位为克
|
|
||||||
|
|
||||||
# 转换为克
|
|
||||||
if unit in ['mg', 'milligram']:
|
|
||||||
mass = value / 1000.0 # mg -> g
|
|
||||||
debug_print(f"🔄 质量转换: {value}mg → {mass}g")
|
|
||||||
elif unit in ['kg', 'kilogram']:
|
|
||||||
mass = value * 1000.0 # kg -> g
|
|
||||||
debug_print(f"🔄 质量转换: {value}kg → {mass}g")
|
|
||||||
else: # g, gram 或默认
|
|
||||||
mass = value # 已经是g
|
|
||||||
debug_print(f"✅ 质量已为g: {mass}g")
|
|
||||||
|
|
||||||
return mass
|
|
||||||
|
|
||||||
def parse_time_input(time_input: Union[str, float]) -> float:
|
|
||||||
"""
|
|
||||||
解析时间输入,支持带单位的字符串
|
|
||||||
|
|
||||||
Args:
|
|
||||||
time_input: 时间输入(如 "1 h", "20 min", "30 s", 60.0)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: 时间(秒)
|
|
||||||
"""
|
|
||||||
if isinstance(time_input, (int, float)):
|
|
||||||
debug_print(f"⏱️ 时间输入为数值: {time_input}秒")
|
|
||||||
return float(time_input)
|
|
||||||
|
|
||||||
if not time_input or not str(time_input).strip():
|
|
||||||
debug_print(f"⚠️ 时间输入为空,返回0秒")
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
time_str = str(time_input).lower().strip()
|
|
||||||
debug_print(f"🔍 解析时间输入: '{time_str}'")
|
|
||||||
|
|
||||||
# 处理未知时间
|
|
||||||
if time_str in ['?', 'unknown', 'tbd']:
|
|
||||||
default_time = 60.0 # 默认1分钟
|
|
||||||
debug_print(f"❓ 检测到未知时间,使用默认值: {default_time}s (1分钟) ⏰")
|
|
||||||
return default_time
|
|
||||||
|
|
||||||
# 移除空格并提取数字和单位
|
|
||||||
time_clean = re.sub(r'\s+', '', time_str)
|
|
||||||
|
|
||||||
# 匹配数字和单位的正则表达式
|
|
||||||
match = re.match(r'([0-9]*\.?[0-9]+)\s*(s|sec|second|min|minute|h|hr|hour|d|day)?', time_clean)
|
|
||||||
|
|
||||||
if not match:
|
|
||||||
debug_print(f"❌ 无法解析时间: '{time_str}',返回0s")
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
value = float(match.group(1))
|
|
||||||
unit = match.group(2) or 's' # 默认单位为秒
|
|
||||||
|
|
||||||
# 转换为秒
|
|
||||||
if unit in ['min', 'minute']:
|
|
||||||
time_sec = value * 60.0 # min -> s
|
|
||||||
debug_print(f"🔄 时间转换: {value}分钟 → {time_sec}秒")
|
|
||||||
elif unit in ['h', 'hr', 'hour']:
|
|
||||||
time_sec = value * 3600.0 # h -> s
|
|
||||||
debug_print(f"🔄 时间转换: {value}小时 → {time_sec}秒")
|
|
||||||
elif unit in ['d', 'day']:
|
|
||||||
time_sec = value * 86400.0 # d -> s
|
|
||||||
debug_print(f"🔄 时间转换: {value}天 → {time_sec}秒")
|
|
||||||
else: # s, sec, second 或默认
|
|
||||||
time_sec = value # 已经是s
|
|
||||||
debug_print(f"✅ 时间已为秒: {time_sec}秒")
|
|
||||||
|
|
||||||
return time_sec
|
|
||||||
|
|
||||||
def find_reagent_vessel(G: nx.DiGraph, reagent: str) -> str:
|
|
||||||
"""增强版试剂容器查找,支持固体和液体"""
|
|
||||||
debug_print(f"🔍 开始查找试剂 '{reagent}' 的容器...")
|
|
||||||
|
|
||||||
# 🔧 方法1:直接搜索 data.reagent_name 和 config.reagent
|
|
||||||
debug_print(f"📋 方法1: 搜索reagent字段...")
|
|
||||||
for node in G.nodes():
|
|
||||||
node_data = G.nodes[node].get('data', {})
|
|
||||||
node_type = G.nodes[node].get('type', '')
|
|
||||||
config_data = G.nodes[node].get('config', {})
|
|
||||||
|
|
||||||
# 只搜索容器类型的节点
|
|
||||||
if node_type == 'container':
|
|
||||||
reagent_name = node_data.get('reagent_name', '').lower()
|
|
||||||
config_reagent = config_data.get('reagent', '').lower()
|
|
||||||
|
|
||||||
# 精确匹配
|
|
||||||
if reagent_name == reagent.lower() or config_reagent == reagent.lower():
|
|
||||||
debug_print(f"✅ 通过reagent字段精确匹配到容器: {node} 🎯")
|
|
||||||
return node
|
|
||||||
|
|
||||||
# 模糊匹配
|
|
||||||
if (reagent.lower() in reagent_name and reagent_name) or \
|
|
||||||
(reagent.lower() in config_reagent and config_reagent):
|
|
||||||
debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node} 🔍")
|
|
||||||
return node
|
|
||||||
|
|
||||||
# 🔧 方法2:常见的容器命名规则
|
|
||||||
debug_print(f"📋 方法2: 使用命名规则查找...")
|
|
||||||
reagent_clean = reagent.lower().replace(' ', '_').replace('-', '_')
|
|
||||||
possible_names = [
|
|
||||||
reagent_clean,
|
|
||||||
f"flask_{reagent_clean}",
|
|
||||||
f"bottle_{reagent_clean}",
|
|
||||||
f"vessel_{reagent_clean}",
|
|
||||||
f"{reagent_clean}_flask",
|
|
||||||
f"{reagent_clean}_bottle",
|
|
||||||
f"reagent_{reagent_clean}",
|
|
||||||
f"reagent_bottle_{reagent_clean}",
|
|
||||||
f"solid_reagent_bottle_{reagent_clean}",
|
|
||||||
f"reagent_bottle_1", # 通用试剂瓶
|
|
||||||
f"reagent_bottle_2",
|
|
||||||
f"reagent_bottle_3"
|
|
||||||
]
|
|
||||||
|
|
||||||
debug_print(f"🔍 尝试的容器名称: {possible_names[:5]}... (共{len(possible_names)}个)")
|
|
||||||
|
|
||||||
for name in possible_names:
|
|
||||||
if name in G.nodes():
|
|
||||||
node_type = G.nodes[name].get('type', '')
|
|
||||||
if node_type == 'container':
|
|
||||||
debug_print(f"✅ 通过命名规则找到容器: {name} 📝")
|
|
||||||
return name
|
|
||||||
|
|
||||||
# 🔧 方法3:节点名称模糊匹配
|
|
||||||
debug_print(f"📋 方法3: 节点名称模糊匹配...")
|
|
||||||
for node_id in G.nodes():
|
|
||||||
node_data = G.nodes[node_id]
|
|
||||||
if node_data.get('type') == 'container':
|
|
||||||
# 检查节点名称是否包含试剂名称
|
|
||||||
if reagent_clean in node_id.lower():
|
|
||||||
debug_print(f"✅ 通过节点名称模糊匹配到容器: {node_id} 🔍")
|
|
||||||
return node_id
|
|
||||||
|
|
||||||
# 检查液体类型匹配
|
|
||||||
vessel_data = node_data.get('data', {})
|
|
||||||
liquids = vessel_data.get('liquid', [])
|
|
||||||
for liquid in liquids:
|
|
||||||
if isinstance(liquid, dict):
|
|
||||||
liquid_type = liquid.get('liquid_type') or liquid.get('name', '')
|
|
||||||
if liquid_type.lower() == reagent.lower():
|
|
||||||
debug_print(f"✅ 通过液体类型匹配到容器: {node_id} 💧")
|
|
||||||
return node_id
|
|
||||||
|
|
||||||
# 🔧 方法4:使用第一个试剂瓶作为备选
|
|
||||||
debug_print(f"📋 方法4: 查找备选试剂瓶...")
|
|
||||||
for node_id in G.nodes():
|
|
||||||
node_data = G.nodes[node_id]
|
|
||||||
if (node_data.get('type') == 'container' and
|
|
||||||
('reagent' in node_id.lower() or 'bottle' in node_id.lower())):
|
|
||||||
debug_print(f"⚠️ 未找到专用容器,使用备选试剂瓶: {node_id} 🔄")
|
|
||||||
return node_id
|
|
||||||
|
|
||||||
debug_print(f"❌ 所有方法都失败了,无法找到容器!")
|
|
||||||
raise ValueError(f"找不到试剂 '{reagent}' 对应的容器")
|
|
||||||
|
|
||||||
def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str:
|
|
||||||
"""查找连接到指定容器的搅拌器"""
|
|
||||||
debug_print(f"🔍 查找连接到容器 '{vessel}' 的搅拌器...")
|
|
||||||
|
|
||||||
stirrer_nodes = []
|
|
||||||
for node in G.nodes():
|
|
||||||
node_class = G.nodes[node].get('class', '').lower()
|
|
||||||
if 'stirrer' in node_class:
|
|
||||||
stirrer_nodes.append(node)
|
|
||||||
debug_print(f"📋 发现搅拌器: {node}")
|
|
||||||
|
|
||||||
debug_print(f"📊 共找到 {len(stirrer_nodes)} 个搅拌器")
|
|
||||||
|
|
||||||
# 查找连接到容器的搅拌器
|
|
||||||
for stirrer in stirrer_nodes:
|
|
||||||
if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
|
|
||||||
debug_print(f"✅ 找到连接的搅拌器: {stirrer} 🔗")
|
|
||||||
return stirrer
|
|
||||||
|
|
||||||
# 返回第一个搅拌器
|
|
||||||
if stirrer_nodes:
|
|
||||||
debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个: {stirrer_nodes[0]} 🔄")
|
|
||||||
return stirrer_nodes[0]
|
|
||||||
|
|
||||||
debug_print(f"❌ 未找到任何搅拌器")
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def find_solid_dispenser(G: nx.DiGraph) -> str:
|
|
||||||
"""查找固体加样器"""
|
|
||||||
debug_print(f"🔍 查找固体加样器...")
|
|
||||||
|
|
||||||
for node in G.nodes():
|
|
||||||
node_class = G.nodes[node].get('class', '').lower()
|
|
||||||
if 'solid_dispenser' in node_class or 'dispenser' in node_class:
|
|
||||||
debug_print(f"✅ 找到固体加样器: {node} 🥄")
|
|
||||||
return node
|
|
||||||
|
|
||||||
debug_print(f"❌ 未找到固体加样器")
|
|
||||||
return ""
|
|
||||||
|
|
||||||
# 🆕 创建进度日志动作
|
# 🆕 创建进度日志动作
|
||||||
def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]:
|
create_action_log = partial(action_log, prefix="[ADD]")
|
||||||
"""创建一个动作日志"""
|
|
||||||
full_message = f"{emoji} {message}"
|
|
||||||
debug_print(full_message)
|
|
||||||
logger.info(full_message)
|
|
||||||
print(f"[ACTION] {full_message}", flush=True)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"action_name": "wait",
|
|
||||||
"action_kwargs": {
|
|
||||||
"time": 0.1,
|
|
||||||
"log_message": full_message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def generate_add_protocol(
|
def generate_add_protocol(
|
||||||
G: nx.DiGraph,
|
G: nx.DiGraph,
|
||||||
@@ -346,16 +57,7 @@ def generate_add_protocol(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# 🔧 核心修改:从字典中提取容器ID
|
# 🔧 核心修改:从字典中提取容器ID
|
||||||
# 统一处理vessel参数
|
vessel_id, vessel_data = get_vessel(vessel)
|
||||||
if isinstance(vessel, dict):
|
|
||||||
if "id" not in vessel:
|
|
||||||
vessel_id = list(vessel.values())[0].get("id", "")
|
|
||||||
else:
|
|
||||||
vessel_id = vessel.get("id", "")
|
|
||||||
vessel_data = vessel.get("data", {})
|
|
||||||
else:
|
|
||||||
vessel_id = str(vessel)
|
|
||||||
vessel_data = G.nodes[vessel_id].get("data", {}) if vessel_id in G.nodes() else {}
|
|
||||||
|
|
||||||
# 🔧 修改:更新容器的液体体积(假设有 liquid_volume 字段)
|
# 🔧 修改:更新容器的液体体积(假设有 liquid_volume 字段)
|
||||||
if "data" in vessel and "liquid_volume" in vessel["data"]:
|
if "data" in vessel and "liquid_volume" in vessel["data"]:
|
||||||
@@ -406,12 +108,7 @@ def generate_add_protocol(
|
|||||||
final_time = parse_time_input(time)
|
final_time = parse_time_input(time)
|
||||||
|
|
||||||
debug_print(f"📊 解析结果:")
|
debug_print(f"📊 解析结果:")
|
||||||
debug_print(f" 📏 体积: {final_volume}mL")
|
debug_print(f" 体积: {final_volume}mL, 质量: {final_mass}g, 时间: {final_time}s, 摩尔: '{mol}', 事件: '{event}', 速率: '{rate_spec}'")
|
||||||
debug_print(f" ⚖️ 质量: {final_mass}g")
|
|
||||||
debug_print(f" ⏱️ 时间: {final_time}s")
|
|
||||||
debug_print(f" 🧬 摩尔: '{mol}'")
|
|
||||||
debug_print(f" 🎯 事件: '{event}'")
|
|
||||||
debug_print(f" ⚡ 速率: '{rate_spec}'")
|
|
||||||
|
|
||||||
# === 判断添加类型 ===
|
# === 判断添加类型 ===
|
||||||
debug_print("🔍 步骤3: 判断添加类型...")
|
debug_print("🔍 步骤3: 判断添加类型...")
|
||||||
|
|||||||
@@ -1,31 +1,15 @@
|
|||||||
import networkx as nx
|
import networkx as nx
|
||||||
import logging
|
import logging
|
||||||
from typing import List, Dict, Any, Union
|
from typing import List, Dict, Any, Union
|
||||||
|
from .utils.vessel_parser import get_vessel
|
||||||
from .pump_protocol import generate_pump_protocol_with_rinsing
|
from .pump_protocol import generate_pump_protocol_with_rinsing
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def debug_print(message):
|
def debug_print(message):
|
||||||
"""调试输出"""
|
"""调试输出"""
|
||||||
print(f"[ADJUST_PH] {message}", flush=True)
|
|
||||||
logger.info(f"[ADJUST_PH] {message}")
|
logger.info(f"[ADJUST_PH] {message}")
|
||||||
|
|
||||||
# 🆕 创建进度日志动作
|
|
||||||
def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]:
|
|
||||||
"""创建一个动作日志"""
|
|
||||||
full_message = f"{emoji} {message}"
|
|
||||||
debug_print(full_message)
|
|
||||||
logger.info(full_message)
|
|
||||||
print(f"[ACTION] {full_message}", flush=True)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"action_name": "wait",
|
|
||||||
"action_kwargs": {
|
|
||||||
"time": 0.1,
|
|
||||||
"log_message": full_message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str:
|
def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str:
|
||||||
"""
|
"""
|
||||||
查找酸碱试剂容器,支持多种匹配模式
|
查找酸碱试剂容器,支持多种匹配模式
|
||||||
@@ -235,16 +219,7 @@ def generate_adjust_ph_protocol(
|
|||||||
List[Dict[str, Any]]: 动作序列
|
List[Dict[str, Any]]: 动作序列
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 统一处理vessel参数
|
vessel_id, vessel_data = get_vessel(vessel)
|
||||||
if isinstance(vessel, dict):
|
|
||||||
if "id" not in vessel:
|
|
||||||
vessel_id = list(vessel.values())[0].get("id", "")
|
|
||||||
else:
|
|
||||||
vessel_id = vessel.get("id", "")
|
|
||||||
vessel_data = vessel.get("data", {})
|
|
||||||
else:
|
|
||||||
vessel_id = str(vessel)
|
|
||||||
vessel_data = G.nodes[vessel_id].get("data", {}) if vessel_id in G.nodes() else {}
|
|
||||||
|
|
||||||
if not vessel_id:
|
if not vessel_id:
|
||||||
debug_print(f"❌ vessel 参数无效,必须包含id字段或直接提供容器ID. vessel: {vessel}")
|
debug_print(f"❌ vessel 参数无效,必须包含id字段或直接提供容器ID. vessel: {vessel}")
|
||||||
|
|||||||
@@ -1,101 +1,9 @@
|
|||||||
from typing import List, Dict, Any
|
from typing import List, Dict, Any
|
||||||
import networkx as nx
|
import networkx as nx
|
||||||
|
from .utils.vessel_parser import get_vessel, find_solvent_vessel
|
||||||
from .pump_protocol import generate_pump_protocol
|
from .pump_protocol import generate_pump_protocol
|
||||||
|
|
||||||
|
|
||||||
def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
|
|
||||||
"""
|
|
||||||
查找溶剂容器,支持多种匹配模式:
|
|
||||||
1. 容器名称匹配(如 flask_water, reagent_bottle_1-DMF)
|
|
||||||
2. 容器内液体类型匹配(如 liquid_type: "DMF", "ethanol")
|
|
||||||
"""
|
|
||||||
print(f"CLEAN_VESSEL: 正在查找溶剂 '{solvent}' 的容器...")
|
|
||||||
|
|
||||||
# 第一步:通过容器名称匹配
|
|
||||||
possible_names = [
|
|
||||||
f"flask_{solvent}", # flask_water, flask_ethanol
|
|
||||||
f"bottle_{solvent}", # bottle_water, bottle_ethanol
|
|
||||||
f"vessel_{solvent}", # vessel_water, vessel_ethanol
|
|
||||||
f"{solvent}_flask", # water_flask, ethanol_flask
|
|
||||||
f"{solvent}_bottle", # water_bottle, ethanol_bottle
|
|
||||||
f"{solvent}", # 直接用溶剂名
|
|
||||||
f"solvent_{solvent}", # solvent_water, solvent_ethanol
|
|
||||||
f"reagent_bottle_{solvent}", # reagent_bottle_DMF
|
|
||||||
]
|
|
||||||
|
|
||||||
# 尝试名称匹配
|
|
||||||
for vessel_name in possible_names:
|
|
||||||
if vessel_name in G.nodes():
|
|
||||||
print(f"CLEAN_VESSEL: 通过名称匹配找到容器: {vessel_name}")
|
|
||||||
return vessel_name
|
|
||||||
|
|
||||||
# 第二步:通过模糊名称匹配(名称中包含溶剂名)
|
|
||||||
for node_id in G.nodes():
|
|
||||||
if G.nodes[node_id].get('type') == 'container':
|
|
||||||
# 检查节点ID或名称中是否包含溶剂名
|
|
||||||
node_name = G.nodes[node_id].get('name', '').lower()
|
|
||||||
if (solvent.lower() in node_id.lower() or
|
|
||||||
solvent.lower() in node_name):
|
|
||||||
print(f"CLEAN_VESSEL: 通过模糊名称匹配找到容器: {node_id} (名称: {node_name})")
|
|
||||||
return node_id
|
|
||||||
|
|
||||||
# 第三步:通过液体类型匹配
|
|
||||||
for node_id in G.nodes():
|
|
||||||
if G.nodes[node_id].get('type') == 'container':
|
|
||||||
vessel_data = G.nodes[node_id].get('data', {})
|
|
||||||
liquids = vessel_data.get('liquid', [])
|
|
||||||
|
|
||||||
for liquid in liquids:
|
|
||||||
if isinstance(liquid, dict):
|
|
||||||
# 支持两种格式的液体类型字段
|
|
||||||
liquid_type = liquid.get('liquid_type') or liquid.get('name', '')
|
|
||||||
reagent_name = vessel_data.get('reagent_name', '')
|
|
||||||
config_reagent = G.nodes[node_id].get('config', {}).get('reagent', '')
|
|
||||||
|
|
||||||
# 检查多个可能的字段
|
|
||||||
if (liquid_type.lower() == solvent.lower() or
|
|
||||||
reagent_name.lower() == solvent.lower() or
|
|
||||||
config_reagent.lower() == solvent.lower()):
|
|
||||||
print(f"CLEAN_VESSEL: 通过液体类型匹配找到容器: {node_id}")
|
|
||||||
print(f" - liquid_type: {liquid_type}")
|
|
||||||
print(f" - reagent_name: {reagent_name}")
|
|
||||||
print(f" - config.reagent: {config_reagent}")
|
|
||||||
return node_id
|
|
||||||
|
|
||||||
# 第四步:列出所有可用的容器信息帮助调试
|
|
||||||
available_containers = []
|
|
||||||
for node_id in G.nodes():
|
|
||||||
if G.nodes[node_id].get('type') == 'container':
|
|
||||||
vessel_data = G.nodes[node_id].get('data', {})
|
|
||||||
config_data = G.nodes[node_id].get('config', {})
|
|
||||||
liquids = vessel_data.get('liquid', [])
|
|
||||||
|
|
||||||
container_info = {
|
|
||||||
'id': node_id,
|
|
||||||
'name': G.nodes[node_id].get('name', ''),
|
|
||||||
'liquid_types': [],
|
|
||||||
'reagent_name': vessel_data.get('reagent_name', ''),
|
|
||||||
'config_reagent': config_data.get('reagent', '')
|
|
||||||
}
|
|
||||||
|
|
||||||
for liquid in liquids:
|
|
||||||
if isinstance(liquid, dict):
|
|
||||||
liquid_type = liquid.get('liquid_type') or liquid.get('name', '')
|
|
||||||
if liquid_type:
|
|
||||||
container_info['liquid_types'].append(liquid_type)
|
|
||||||
|
|
||||||
available_containers.append(container_info)
|
|
||||||
|
|
||||||
print(f"CLEAN_VESSEL: 可用容器列表:")
|
|
||||||
for container in available_containers:
|
|
||||||
print(f" - {container['id']}: {container['name']}")
|
|
||||||
print(f" 液体类型: {container['liquid_types']}")
|
|
||||||
print(f" 试剂名称: {container['reagent_name']}")
|
|
||||||
print(f" 配置试剂: {container['config_reagent']}")
|
|
||||||
|
|
||||||
raise ValueError(f"未找到溶剂 '{solvent}' 的容器。尝试了名称匹配: {possible_names}")
|
|
||||||
|
|
||||||
|
|
||||||
def find_solvent_vessel_by_any_match(G: nx.DiGraph, solvent: str) -> str:
|
def find_solvent_vessel_by_any_match(G: nx.DiGraph, solvent: str) -> str:
|
||||||
"""
|
"""
|
||||||
增强版溶剂容器查找,支持各种匹配方式的别名函数
|
增强版溶剂容器查找,支持各种匹配方式的别名函数
|
||||||
@@ -181,16 +89,7 @@ def generate_clean_vessel_protocol(
|
|||||||
clean_protocol = generate_clean_vessel_protocol(G, {"id": "main_reactor"}, "water", 100.0, 60.0, 2)
|
clean_protocol = generate_clean_vessel_protocol(G, {"id": "main_reactor"}, "water", 100.0, 60.0, 2)
|
||||||
"""
|
"""
|
||||||
# 🔧 核心修改:从字典中提取容器ID
|
# 🔧 核心修改:从字典中提取容器ID
|
||||||
# 统一处理vessel参数
|
vessel_id, vessel_data = get_vessel(vessel)
|
||||||
if isinstance(vessel, dict):
|
|
||||||
if "id" not in vessel:
|
|
||||||
vessel_id = list(vessel.values())[0].get("id", "")
|
|
||||||
else:
|
|
||||||
vessel_id = vessel.get("id", "")
|
|
||||||
vessel_data = vessel.get("data", {})
|
|
||||||
else:
|
|
||||||
vessel_id = str(vessel)
|
|
||||||
vessel_data = G.nodes[vessel_id].get("data", {}) if vessel_id in G.nodes() else {}
|
|
||||||
|
|
||||||
action_sequence = []
|
action_sequence = []
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +1,22 @@
|
|||||||
|
from functools import partial
|
||||||
|
|
||||||
import networkx as nx
|
import networkx as nx
|
||||||
import re
|
import re
|
||||||
import logging
|
import logging
|
||||||
from typing import List, Dict, Any, Union
|
from typing import List, Dict, Any, Union
|
||||||
|
|
||||||
|
from .utils.vessel_parser import get_vessel
|
||||||
|
from .utils.logger_util import action_log
|
||||||
from .pump_protocol import generate_pump_protocol_with_rinsing
|
from .pump_protocol import generate_pump_protocol_with_rinsing
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def debug_print(message):
|
def debug_print(message):
|
||||||
"""调试输出"""
|
"""调试输出"""
|
||||||
print(f"[DISSOLVE] {message}", flush=True)
|
|
||||||
logger.info(f"[DISSOLVE] {message}")
|
logger.info(f"[DISSOLVE] {message}")
|
||||||
|
|
||||||
# 🆕 创建进度日志动作
|
# 🆕 创建进度日志动作
|
||||||
def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]:
|
create_action_log = partial(action_log, prefix="[DISSOLVE]")
|
||||||
"""创建一个动作日志"""
|
|
||||||
full_message = f"{emoji} {message}"
|
|
||||||
debug_print(full_message)
|
|
||||||
logger.info(full_message)
|
|
||||||
print(f"[ACTION] {full_message}", flush=True)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"action_name": "wait",
|
|
||||||
"action_kwargs": {
|
|
||||||
"time": 0.1,
|
|
||||||
"log_message": full_message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def parse_volume_input(volume_input: Union[str, float]) -> float:
|
def parse_volume_input(volume_input: Union[str, float]) -> float:
|
||||||
"""
|
"""
|
||||||
@@ -446,7 +437,7 @@ def generate_dissolve_protocol(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# 🔧 核心修改:从字典中提取容器ID
|
# 🔧 核心修改:从字典中提取容器ID
|
||||||
vessel_id = vessel["id"]
|
vessel_id, vessel_data = get_vessel(vessel)
|
||||||
|
|
||||||
debug_print("=" * 60)
|
debug_print("=" * 60)
|
||||||
debug_print("🧪 开始生成溶解协议")
|
debug_print("🧪 开始生成溶解协议")
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import networkx as nx
|
import networkx as nx
|
||||||
from typing import List, Dict, Any
|
from typing import List, Dict, Any
|
||||||
|
|
||||||
|
from unilabos.compile.utils.vessel_parser import get_vessel
|
||||||
|
|
||||||
|
|
||||||
def find_connected_heater(G: nx.DiGraph, vessel: str) -> str:
|
def find_connected_heater(G: nx.DiGraph, vessel: str) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -63,7 +65,7 @@ def generate_dry_protocol(
|
|||||||
List[Dict[str, Any]]: 动作序列
|
List[Dict[str, Any]]: 动作序列
|
||||||
"""
|
"""
|
||||||
# 🔧 核心修改:从字典中提取容器ID
|
# 🔧 核心修改:从字典中提取容器ID
|
||||||
vessel_id = vessel["id"]
|
vessel_id, vessel_data = get_vessel(vessel)
|
||||||
|
|
||||||
action_sequence = []
|
action_sequence = []
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
from functools import partial
|
||||||
|
|
||||||
import networkx as nx
|
import networkx as nx
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
import sys
|
import sys
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional
|
||||||
|
from .utils.vessel_parser import get_vessel
|
||||||
|
from .utils.logger_util import action_log
|
||||||
from .pump_protocol import generate_pump_protocol_with_rinsing, generate_pump_protocol
|
from .pump_protocol import generate_pump_protocol_with_rinsing, generate_pump_protocol
|
||||||
|
|
||||||
# 设置日志
|
# 设置日志
|
||||||
@@ -21,48 +25,17 @@ def debug_print(message):
|
|||||||
try:
|
try:
|
||||||
# 确保消息是字符串格式
|
# 确保消息是字符串格式
|
||||||
safe_message = str(message)
|
safe_message = str(message)
|
||||||
print(f"[抽真空充气] {safe_message}", flush=True)
|
|
||||||
logger.info(f"[抽真空充气] {safe_message}")
|
logger.info(f"[抽真空充气] {safe_message}")
|
||||||
except UnicodeEncodeError:
|
except UnicodeEncodeError:
|
||||||
# 如果编码失败,尝试替换不支持的字符
|
# 如果编码失败,尝试替换不支持的字符
|
||||||
safe_message = str(message).encode('utf-8', errors='replace').decode('utf-8')
|
safe_message = str(message).encode('utf-8', errors='replace').decode('utf-8')
|
||||||
print(f"[抽真空充气] {safe_message}", flush=True)
|
|
||||||
logger.info(f"[抽真空充气] {safe_message}")
|
logger.info(f"[抽真空充气] {safe_message}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 最后的安全措施
|
# 最后的安全措施
|
||||||
fallback_message = f"日志输出错误: {repr(message)}"
|
fallback_message = f"日志输出错误: {repr(message)}"
|
||||||
print(f"[抽真空充气] {fallback_message}", flush=True)
|
|
||||||
logger.info(f"[抽真空充气] {fallback_message}")
|
logger.info(f"[抽真空充气] {fallback_message}")
|
||||||
|
|
||||||
def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]:
|
create_action_log = partial(action_log, prefix="[抽真空充气]")
|
||||||
"""创建一个动作日志 - 支持中文和emoji"""
|
|
||||||
try:
|
|
||||||
full_message = f"{emoji} {message}"
|
|
||||||
debug_print(full_message)
|
|
||||||
logger.info(full_message)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"action_name": "wait",
|
|
||||||
"action_kwargs": {
|
|
||||||
"time": 0.1,
|
|
||||||
"log_message": full_message,
|
|
||||||
"progress_message": full_message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
# 如果emoji有问题,使用纯文本
|
|
||||||
safe_message = f"[日志] {message}"
|
|
||||||
debug_print(safe_message)
|
|
||||||
logger.info(safe_message)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"action_name": "wait",
|
|
||||||
"action_kwargs": {
|
|
||||||
"time": 0.1,
|
|
||||||
"log_message": safe_message,
|
|
||||||
"progress_message": safe_message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def find_gas_source(G: nx.DiGraph, gas: str) -> str:
|
def find_gas_source(G: nx.DiGraph, gas: str) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -288,16 +261,7 @@ def generate_evacuateandrefill_protocol(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# 🔧 核心修改:从字典中提取容器ID
|
# 🔧 核心修改:从字典中提取容器ID
|
||||||
# 统一处理vessel参数
|
vessel_id, vessel_data = get_vessel(vessel)
|
||||||
if isinstance(vessel, dict):
|
|
||||||
if "id" not in vessel:
|
|
||||||
vessel_id = list(vessel.values())[0].get("id", "")
|
|
||||||
else:
|
|
||||||
vessel_id = vessel.get("id", "")
|
|
||||||
vessel_data = vessel.get("data", {})
|
|
||||||
else:
|
|
||||||
vessel_id = str(vessel)
|
|
||||||
vessel_data = G.nodes[vessel_id].get("data", {}) if vessel_id in G.nodes() else {}
|
|
||||||
|
|
||||||
# 硬编码重复次数为 3
|
# 硬编码重复次数为 3
|
||||||
repeats = 3
|
repeats = 3
|
||||||
|
|||||||
@@ -2,75 +2,15 @@ from typing import List, Dict, Any, Optional, Union
|
|||||||
import networkx as nx
|
import networkx as nx
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
from .utils.vessel_parser import get_vessel
|
||||||
|
from .utils.unit_parser import parse_time_input
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def debug_print(message):
|
def debug_print(message):
|
||||||
"""调试输出"""
|
"""调试输出"""
|
||||||
print(f"🧪 [EVAPORATE] {message}", flush=True)
|
|
||||||
logger.info(f"[EVAPORATE] {message}")
|
logger.info(f"[EVAPORATE] {message}")
|
||||||
|
|
||||||
def parse_time_input(time_input: Union[str, float]) -> float:
|
|
||||||
"""
|
|
||||||
解析时间输入,支持带单位的字符串
|
|
||||||
|
|
||||||
Args:
|
|
||||||
time_input: 时间输入(如 "3 min", "180", "0.5 h" 等)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: 时间(秒)
|
|
||||||
"""
|
|
||||||
if isinstance(time_input, (int, float)):
|
|
||||||
debug_print(f"⏱️ 时间输入为数字: {time_input}s ✨")
|
|
||||||
return float(time_input) # 🔧 确保返回float
|
|
||||||
|
|
||||||
if not time_input or not str(time_input).strip():
|
|
||||||
debug_print(f"⚠️ 时间输入为空,使用默认值: 180s (3分钟) 🕐")
|
|
||||||
return 180.0 # 默认3分钟
|
|
||||||
|
|
||||||
time_str = str(time_input).lower().strip()
|
|
||||||
debug_print(f"🔍 解析时间输入: '{time_str}' 📝")
|
|
||||||
|
|
||||||
# 处理未知时间
|
|
||||||
if time_str in ['?', 'unknown', 'tbd']:
|
|
||||||
default_time = 180.0 # 默认3分钟
|
|
||||||
debug_print(f"❓ 检测到未知时间,使用默认值: {default_time}s (3分钟) 🤷♀️")
|
|
||||||
return default_time
|
|
||||||
|
|
||||||
# 移除空格并提取数字和单位
|
|
||||||
time_clean = re.sub(r'\s+', '', time_str)
|
|
||||||
|
|
||||||
# 匹配数字和单位的正则表达式
|
|
||||||
match = re.match(r'([0-9]*\.?[0-9]+)\s*(s|sec|second|min|minute|h|hr|hour|d|day)?', time_clean)
|
|
||||||
|
|
||||||
if not match:
|
|
||||||
# 如果无法解析,尝试直接转换为数字(默认秒)
|
|
||||||
try:
|
|
||||||
value = float(time_str)
|
|
||||||
debug_print(f"✅ 时间解析成功: {time_str} → {value}s(无单位,默认秒)⏰")
|
|
||||||
return float(value) # 🔧 确保返回float
|
|
||||||
except ValueError:
|
|
||||||
debug_print(f"❌ 无法解析时间: '{time_str}',使用默认值180s (3分钟) 😅")
|
|
||||||
return 180.0
|
|
||||||
|
|
||||||
value = float(match.group(1))
|
|
||||||
unit = match.group(2) or 's' # 默认单位为秒
|
|
||||||
|
|
||||||
# 转换为秒
|
|
||||||
if unit in ['min', 'minute']:
|
|
||||||
time_sec = value * 60.0 # min -> s
|
|
||||||
debug_print(f"🕐 时间转换: {value} 分钟 → {time_sec}s ⏰")
|
|
||||||
elif unit in ['h', 'hr', 'hour']:
|
|
||||||
time_sec = value * 3600.0 # h -> s
|
|
||||||
debug_print(f"🕐 时间转换: {value} 小时 → {time_sec}s ({time_sec/60:.1f}分钟) ⏰")
|
|
||||||
elif unit in ['d', 'day']:
|
|
||||||
time_sec = value * 86400.0 # d -> s
|
|
||||||
debug_print(f"🕐 时间转换: {value} 天 → {time_sec}s ({time_sec/3600:.1f}小时) ⏰")
|
|
||||||
else: # s, sec, second 或默认
|
|
||||||
time_sec = value # 已经是s
|
|
||||||
debug_print(f"🕐 时间转换: {value}s → {time_sec}s (已是秒) ⏰")
|
|
||||||
|
|
||||||
return float(time_sec) # 🔧 确保返回float
|
|
||||||
|
|
||||||
def find_rotavap_device(G: nx.DiGraph, vessel: str = None) -> Optional[str]:
|
def find_rotavap_device(G: nx.DiGraph, vessel: str = None) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
@@ -201,16 +141,7 @@ def generate_evaporate_protocol(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# 🔧 核心修改:从字典中提取容器ID
|
# 🔧 核心修改:从字典中提取容器ID
|
||||||
# 统一处理vessel参数
|
vessel_id, vessel_data = get_vessel(vessel)
|
||||||
if isinstance(vessel, dict):
|
|
||||||
if "id" not in vessel:
|
|
||||||
vessel_id = list(vessel.values())[0].get("id", "")
|
|
||||||
else:
|
|
||||||
vessel_id = vessel.get("id", "")
|
|
||||||
vessel_data = vessel.get("data", {})
|
|
||||||
else:
|
|
||||||
vessel_id = str(vessel)
|
|
||||||
vessel_data = G.nodes[vessel_id].get("data", {}) if vessel_id in G.nodes() else {}
|
|
||||||
|
|
||||||
debug_print("🌟" * 20)
|
debug_print("🌟" * 20)
|
||||||
debug_print("🌪️ 开始生成蒸发协议(支持单位和体积运算)✨")
|
debug_print("🌪️ 开始生成蒸发协议(支持单位和体积运算)✨")
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional
|
||||||
import networkx as nx
|
import networkx as nx
|
||||||
import logging
|
import logging
|
||||||
|
from .utils.vessel_parser import get_vessel
|
||||||
from .pump_protocol import generate_pump_protocol_with_rinsing
|
from .pump_protocol import generate_pump_protocol_with_rinsing
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def debug_print(message):
|
def debug_print(message):
|
||||||
"""调试输出"""
|
"""调试输出"""
|
||||||
print(f"🧪 [FILTER] {message}", flush=True)
|
|
||||||
logger.info(f"[FILTER] {message}")
|
logger.info(f"[FILTER] {message}")
|
||||||
|
|
||||||
def find_filter_device(G: nx.DiGraph) -> str:
|
def find_filter_device(G: nx.DiGraph) -> str:
|
||||||
@@ -51,7 +51,7 @@ def validate_vessel(G: nx.DiGraph, vessel: str, vessel_type: str = "容器") ->
|
|||||||
def generate_filter_protocol(
|
def generate_filter_protocol(
|
||||||
G: nx.DiGraph,
|
G: nx.DiGraph,
|
||||||
vessel: dict, # 🔧 修改:从字符串改为字典类型
|
vessel: dict, # 🔧 修改:从字符串改为字典类型
|
||||||
filtrate_vessel: str = "",
|
filtrate_vessel: dict = {"id": "waste"},
|
||||||
**kwargs
|
**kwargs
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
@@ -68,16 +68,8 @@ def generate_filter_protocol(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# 🔧 核心修改:从字典中提取容器ID
|
# 🔧 核心修改:从字典中提取容器ID
|
||||||
# 统一处理vessel参数
|
vessel_id, vessel_data = get_vessel(vessel)
|
||||||
if isinstance(vessel, dict):
|
filtrate_vessel_id, filtrate_vessel_data = get_vessel(filtrate_vessel)
|
||||||
if "id" not in vessel:
|
|
||||||
vessel_id = list(vessel.values())[0].get("id", "")
|
|
||||||
else:
|
|
||||||
vessel_id = vessel.get("id", "")
|
|
||||||
vessel_data = vessel.get("data", {})
|
|
||||||
else:
|
|
||||||
vessel_id = str(vessel)
|
|
||||||
vessel_data = G.nodes[vessel_id].get("data", {}) if vessel_id in G.nodes() else {}
|
|
||||||
|
|
||||||
debug_print("🌊" * 20)
|
debug_print("🌊" * 20)
|
||||||
debug_print("🚀 开始生成过滤协议(支持体积运算)✨")
|
debug_print("🚀 开始生成过滤协议(支持体积运算)✨")
|
||||||
@@ -111,7 +103,7 @@ def generate_filter_protocol(
|
|||||||
# 验证可选参数
|
# 验证可选参数
|
||||||
debug_print(" 🔍 验证可选参数...")
|
debug_print(" 🔍 验证可选参数...")
|
||||||
if filtrate_vessel:
|
if filtrate_vessel:
|
||||||
validate_vessel(G, filtrate_vessel, "滤液容器")
|
validate_vessel(G, filtrate_vessel_id, "滤液容器")
|
||||||
debug_print(" 🌊 模式: 过滤并收集滤液 💧")
|
debug_print(" 🌊 模式: 过滤并收集滤液 💧")
|
||||||
else:
|
else:
|
||||||
debug_print(" 🧱 模式: 过滤并收集固体 🔬")
|
debug_print(" 🧱 模式: 过滤并收集固体 🔬")
|
||||||
@@ -168,8 +160,8 @@ def generate_filter_protocol(
|
|||||||
# 使用pump protocol转移液体到过滤器
|
# 使用pump protocol转移液体到过滤器
|
||||||
transfer_actions = generate_pump_protocol_with_rinsing(
|
transfer_actions = generate_pump_protocol_with_rinsing(
|
||||||
G=G,
|
G=G,
|
||||||
from_vessel=vessel_id, # 🔧 使用 vessel_id
|
from_vessel={"id": vessel_id}, # 🔧 使用 vessel_id
|
||||||
to_vessel=filter_device,
|
to_vessel={"id": filter_device},
|
||||||
volume=0.0, # 转移所有液体
|
volume=0.0, # 转移所有液体
|
||||||
amount="",
|
amount="",
|
||||||
time=0.0,
|
time=0.0,
|
||||||
@@ -220,8 +212,8 @@ def generate_filter_protocol(
|
|||||||
# 构建过滤动作参数
|
# 构建过滤动作参数
|
||||||
debug_print(" ⚙️ 构建过滤参数...")
|
debug_print(" ⚙️ 构建过滤参数...")
|
||||||
filter_kwargs = {
|
filter_kwargs = {
|
||||||
"vessel": filter_device, # 过滤器设备
|
"vessel": {"id": filter_device}, # 过滤器设备
|
||||||
"filtrate_vessel": filtrate_vessel, # 滤液容器(可能为空)
|
"filtrate_vessel": {"id": filtrate_vessel_id}, # 滤液容器(可能为空)
|
||||||
"stir": kwargs.get("stir", False),
|
"stir": kwargs.get("stir", False),
|
||||||
"stir_speed": kwargs.get("stir_speed", 0.0),
|
"stir_speed": kwargs.get("stir_speed", 0.0),
|
||||||
"temp": kwargs.get("temp", 25.0),
|
"temp": kwargs.get("temp", 25.0),
|
||||||
@@ -252,8 +244,8 @@ def generate_filter_protocol(
|
|||||||
# === 收集滤液(如果需要)===
|
# === 收集滤液(如果需要)===
|
||||||
debug_print("📍 步骤5: 收集滤液... 💧")
|
debug_print("📍 步骤5: 收集滤液... 💧")
|
||||||
|
|
||||||
if filtrate_vessel:
|
if filtrate_vessel_id and filtrate_vessel_id not in G.neighbors(filter_device):
|
||||||
debug_print(f" 🧪 收集滤液: {filter_device} → {filtrate_vessel} 💧")
|
debug_print(f" 🧪 收集滤液: {filter_device} → {filtrate_vessel_id} 💧")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
debug_print(" 🔄 开始执行收集操作...")
|
debug_print(" 🔄 开始执行收集操作...")
|
||||||
@@ -282,20 +274,20 @@ def generate_filter_protocol(
|
|||||||
debug_print(" 🔧 更新滤液容器体积...")
|
debug_print(" 🔧 更新滤液容器体积...")
|
||||||
|
|
||||||
# 更新filtrate_vessel在图中的体积(如果它是节点)
|
# 更新filtrate_vessel在图中的体积(如果它是节点)
|
||||||
if filtrate_vessel in G.nodes():
|
if filtrate_vessel_id in G.nodes():
|
||||||
if 'data' not in G.nodes[filtrate_vessel]:
|
if 'data' not in G.nodes[filtrate_vessel_id]:
|
||||||
G.nodes[filtrate_vessel]['data'] = {}
|
G.nodes[filtrate_vessel_id]['data'] = {}
|
||||||
|
|
||||||
current_filtrate_volume = G.nodes[filtrate_vessel]['data'].get('liquid_volume', 0.0)
|
current_filtrate_volume = G.nodes[filtrate_vessel_id]['data'].get('liquid_volume', 0.0)
|
||||||
if isinstance(current_filtrate_volume, list):
|
if isinstance(current_filtrate_volume, list):
|
||||||
if len(current_filtrate_volume) > 0:
|
if len(current_filtrate_volume) > 0:
|
||||||
G.nodes[filtrate_vessel]['data']['liquid_volume'][0] += expected_filtrate_volume
|
G.nodes[filtrate_vessel_id]['data']['liquid_volume'][0] += expected_filtrate_volume
|
||||||
else:
|
else:
|
||||||
G.nodes[filtrate_vessel]['data']['liquid_volume'] = [expected_filtrate_volume]
|
G.nodes[filtrate_vessel_id]['data']['liquid_volume'] = [expected_filtrate_volume]
|
||||||
else:
|
else:
|
||||||
G.nodes[filtrate_vessel]['data']['liquid_volume'] = current_filtrate_volume + expected_filtrate_volume
|
G.nodes[filtrate_vessel_id]['data']['liquid_volume'] = current_filtrate_volume + expected_filtrate_volume
|
||||||
|
|
||||||
debug_print(f" 📊 滤液容器 {filtrate_vessel} 体积增加 {expected_filtrate_volume:.2f}mL")
|
debug_print(f" 📊 滤液容器 {filtrate_vessel_id} 体积增加 {expected_filtrate_volume:.2f}mL")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
debug_print(" ⚠️ 收集协议返回空序列 🤔")
|
debug_print(" ⚠️ 收集协议返回空序列 🤔")
|
||||||
@@ -360,7 +352,7 @@ def generate_filter_protocol(
|
|||||||
debug_print(f"📊 总动作数: {len(action_sequence)} 个 📝")
|
debug_print(f"📊 总动作数: {len(action_sequence)} 个 📝")
|
||||||
debug_print(f"🥽 过滤容器: {vessel_id} 🧪")
|
debug_print(f"🥽 过滤容器: {vessel_id} 🧪")
|
||||||
debug_print(f"🌊 过滤器设备: {filter_device} 🔧")
|
debug_print(f"🌊 过滤器设备: {filter_device} 🔧")
|
||||||
debug_print(f"💧 滤液容器: {filtrate_vessel or '无(保留固体)'} 🧱")
|
debug_print(f"💧 滤液容器: {filtrate_vessel_id or '无(保留固体)'} 🧱")
|
||||||
debug_print(f"⏱️ 预计总时间: {(len(action_sequence) * 5):.0f} 秒 ⌛")
|
debug_print(f"⏱️ 预计总时间: {(len(action_sequence) * 5):.0f} 秒 ⌛")
|
||||||
if original_liquid_volume > 0:
|
if original_liquid_volume > 0:
|
||||||
debug_print(f"📊 体积变化统计:")
|
debug_print(f"📊 体积变化统计:")
|
||||||
@@ -372,4 +364,3 @@ def generate_filter_protocol(
|
|||||||
debug_print("🎊" * 20)
|
debug_print("🎊" * 20)
|
||||||
|
|
||||||
return action_sequence
|
return action_sequence
|
||||||
|
|
||||||
|
|||||||
@@ -2,81 +2,15 @@ from typing import List, Dict, Any, Union
|
|||||||
import networkx as nx
|
import networkx as nx
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
from .utils.vessel_parser import get_vessel
|
||||||
|
from .utils.unit_parser import parse_time_input
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def debug_print(message):
|
def debug_print(message):
|
||||||
"""调试输出"""
|
"""调试输出"""
|
||||||
print(f"🌡️ [HEATCHILL] {message}", flush=True)
|
|
||||||
logger.info(f"[HEATCHILL] {message}")
|
logger.info(f"[HEATCHILL] {message}")
|
||||||
|
|
||||||
def parse_time_input(time_input: Union[str, float, int]) -> float:
|
|
||||||
"""
|
|
||||||
解析时间输入(统一函数)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
time_input: 时间输入(如 "30 min", "1 h", "300", "?", 60.0)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: 时间(秒)
|
|
||||||
"""
|
|
||||||
if not time_input:
|
|
||||||
return 300.0
|
|
||||||
|
|
||||||
# 🔢 处理数值输入
|
|
||||||
if isinstance(time_input, (int, float)):
|
|
||||||
result = float(time_input)
|
|
||||||
debug_print(f"⏰ 数值时间: {time_input} → {result}s")
|
|
||||||
return result
|
|
||||||
|
|
||||||
# 📝 处理字符串输入
|
|
||||||
time_str = str(time_input).lower().strip()
|
|
||||||
debug_print(f"🔍 解析时间: '{time_str}'")
|
|
||||||
|
|
||||||
# ❓ 特殊值处理
|
|
||||||
special_times = {
|
|
||||||
'?': 300.0, 'unknown': 300.0, 'tbd': 300.0,
|
|
||||||
'overnight': 43200.0, 'several hours': 10800.0,
|
|
||||||
'few hours': 7200.0, 'long time': 3600.0, 'short time': 300.0
|
|
||||||
}
|
|
||||||
|
|
||||||
if time_str in special_times:
|
|
||||||
result = special_times[time_str]
|
|
||||||
debug_print(f"🎯 特殊时间: '{time_str}' → {result}s ({result/60:.1f}分钟)")
|
|
||||||
return result
|
|
||||||
|
|
||||||
# 🔢 纯数字处理
|
|
||||||
try:
|
|
||||||
result = float(time_str)
|
|
||||||
debug_print(f"⏰ 纯数字: {time_str} → {result}s")
|
|
||||||
return result
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 📐 正则表达式解析
|
|
||||||
pattern = r'(\d+\.?\d*)\s*([a-z]*)'
|
|
||||||
match = re.match(pattern, time_str)
|
|
||||||
|
|
||||||
if not match:
|
|
||||||
debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值: 300s")
|
|
||||||
return 300.0
|
|
||||||
|
|
||||||
value = float(match.group(1))
|
|
||||||
unit = match.group(2) or 's'
|
|
||||||
|
|
||||||
# 📏 单位转换
|
|
||||||
unit_multipliers = {
|
|
||||||
's': 1.0, 'sec': 1.0, 'second': 1.0, 'seconds': 1.0,
|
|
||||||
'm': 60.0, 'min': 60.0, 'mins': 60.0, 'minute': 60.0, 'minutes': 60.0,
|
|
||||||
'h': 3600.0, 'hr': 3600.0, 'hrs': 3600.0, 'hour': 3600.0, 'hours': 3600.0,
|
|
||||||
'd': 86400.0, 'day': 86400.0, 'days': 86400.0
|
|
||||||
}
|
|
||||||
|
|
||||||
multiplier = unit_multipliers.get(unit, 1.0)
|
|
||||||
result = value * multiplier
|
|
||||||
|
|
||||||
debug_print(f"✅ 时间解析: '{time_str}' → {value} {unit} → {result}s ({result/60:.1f}分钟)")
|
|
||||||
return result
|
|
||||||
|
|
||||||
def parse_temp_input(temp_input: Union[str, float], default_temp: float = 25.0) -> float:
|
def parse_temp_input(temp_input: Union[str, float], default_temp: float = 25.0) -> float:
|
||||||
"""
|
"""
|
||||||
@@ -217,16 +151,7 @@ def generate_heat_chill_protocol(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# 🔧 核心修改:从字典中提取容器ID
|
# 🔧 核心修改:从字典中提取容器ID
|
||||||
# 统一处理vessel参数
|
vessel_id, vessel_data = get_vessel(vessel)
|
||||||
if isinstance(vessel, dict):
|
|
||||||
if "id" not in vessel:
|
|
||||||
vessel_id = list(vessel.values())[0].get("id", "")
|
|
||||||
else:
|
|
||||||
vessel_id = vessel.get("id", "")
|
|
||||||
vessel_data = vessel.get("data", {})
|
|
||||||
else:
|
|
||||||
vessel_id = str(vessel)
|
|
||||||
vessel_data = G.nodes[vessel_id].get("data", {}) if vessel_id in G.nodes() else {}
|
|
||||||
|
|
||||||
debug_print("🌡️" * 20)
|
debug_print("🌡️" * 20)
|
||||||
debug_print("🚀 开始生成加热冷却协议(支持vessel字典)✨")
|
debug_print("🚀 开始生成加热冷却协议(支持vessel字典)✨")
|
||||||
@@ -295,7 +220,7 @@ def generate_heat_chill_protocol(
|
|||||||
"device_id": heatchill_id,
|
"device_id": heatchill_id,
|
||||||
"action_name": "heat_chill",
|
"action_name": "heat_chill",
|
||||||
"action_kwargs": {
|
"action_kwargs": {
|
||||||
"vessel": vessel_id, # 🔧 使用 vessel_id
|
"vessel": vessel,
|
||||||
"temp": float(final_temp),
|
"temp": float(final_temp),
|
||||||
"time": float(final_time),
|
"time": float(final_time),
|
||||||
"stir": bool(stir),
|
"stir": bool(stir),
|
||||||
@@ -329,7 +254,7 @@ def generate_heat_chill_to_temp_protocol(
|
|||||||
**kwargs
|
**kwargs
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
"""生成加热到指定温度的协议(简化版)"""
|
"""生成加热到指定温度的协议(简化版)"""
|
||||||
vessel_id = vessel["id"]
|
vessel_id, _ = get_vessel(vessel)
|
||||||
debug_print(f"🌡️ 生成加热到温度协议: {vessel_id} → {temp}°C")
|
debug_print(f"🌡️ 生成加热到温度协议: {vessel_id} → {temp}°C")
|
||||||
return generate_heat_chill_protocol(G, vessel, temp, time, **kwargs)
|
return generate_heat_chill_protocol(G, vessel, temp, time, **kwargs)
|
||||||
|
|
||||||
@@ -343,7 +268,7 @@ def generate_heat_chill_start_protocol(
|
|||||||
"""生成开始加热操作的协议序列"""
|
"""生成开始加热操作的协议序列"""
|
||||||
|
|
||||||
# 🔧 核心修改:从字典中提取容器ID
|
# 🔧 核心修改:从字典中提取容器ID
|
||||||
vessel_id = vessel["id"]
|
vessel_id, _ = get_vessel(vessel)
|
||||||
|
|
||||||
debug_print("🔥 开始生成启动加热协议 ✨")
|
debug_print("🔥 开始生成启动加热协议 ✨")
|
||||||
debug_print(f"🥽 vessel: {vessel} (ID: {vessel_id}), 🌡️ temp: {temp}°C")
|
debug_print(f"🥽 vessel: {vessel} (ID: {vessel_id}), 🌡️ temp: {temp}°C")
|
||||||
@@ -361,7 +286,6 @@ def generate_heat_chill_start_protocol(
|
|||||||
"device_id": heatchill_id,
|
"device_id": heatchill_id,
|
||||||
"action_name": "heat_chill_start",
|
"action_name": "heat_chill_start",
|
||||||
"action_kwargs": {
|
"action_kwargs": {
|
||||||
"vessel": vessel_id, # 🔧 使用 vessel_id
|
|
||||||
"temp": temp,
|
"temp": temp,
|
||||||
"purpose": purpose or f"开始加热到 {temp}°C"
|
"purpose": purpose or f"开始加热到 {temp}°C"
|
||||||
}
|
}
|
||||||
@@ -378,7 +302,7 @@ def generate_heat_chill_stop_protocol(
|
|||||||
"""生成停止加热操作的协议序列"""
|
"""生成停止加热操作的协议序列"""
|
||||||
|
|
||||||
# 🔧 核心修改:从字典中提取容器ID
|
# 🔧 核心修改:从字典中提取容器ID
|
||||||
vessel_id = vessel["id"]
|
vessel_id, _ = get_vessel(vessel)
|
||||||
|
|
||||||
debug_print("🛑 开始生成停止加热协议 ✨")
|
debug_print("🛑 开始生成停止加热协议 ✨")
|
||||||
debug_print(f"🥽 vessel: {vessel} (ID: {vessel_id})")
|
debug_print(f"🥽 vessel: {vessel} (ID: {vessel_id})")
|
||||||
@@ -396,10 +320,8 @@ def generate_heat_chill_stop_protocol(
|
|||||||
"device_id": heatchill_id,
|
"device_id": heatchill_id,
|
||||||
"action_name": "heat_chill_stop",
|
"action_name": "heat_chill_stop",
|
||||||
"action_kwargs": {
|
"action_kwargs": {
|
||||||
"vessel": vessel_id # 🔧 使用 vessel_id
|
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
|
|
||||||
debug_print(f"✅ 停止加热协议生成完成 🎯")
|
debug_print(f"✅ 停止加热协议生成完成 🎯")
|
||||||
return action_sequence
|
return action_sequence
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import networkx as nx
|
import networkx as nx
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional
|
||||||
|
from .utils.vessel_parser import get_vessel
|
||||||
|
|
||||||
|
|
||||||
def parse_temperature(temp_str: str) -> float:
|
def parse_temperature(temp_str: str) -> float:
|
||||||
@@ -170,16 +171,7 @@ def generate_hydrogenate_protocol(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# 🔧 核心修改:从字典中提取容器ID
|
# 🔧 核心修改:从字典中提取容器ID
|
||||||
# 统一处理vessel参数
|
vessel_id, vessel_data = get_vessel(vessel)
|
||||||
if isinstance(vessel, dict):
|
|
||||||
if "id" not in vessel:
|
|
||||||
vessel_id = list(vessel.values())[0].get("id", "")
|
|
||||||
else:
|
|
||||||
vessel_id = vessel.get("id", "")
|
|
||||||
vessel_data = vessel.get("data", {})
|
|
||||||
else:
|
|
||||||
vessel_id = str(vessel)
|
|
||||||
vessel_data = G.nodes[vessel_id].get("data", {}) if vessel_id in G.nodes() else {}
|
|
||||||
|
|
||||||
action_sequence = []
|
action_sequence = []
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,91 +2,17 @@ import networkx as nx
|
|||||||
import re
|
import re
|
||||||
import logging
|
import logging
|
||||||
from typing import List, Dict, Any, Tuple, Union
|
from typing import List, Dict, Any, Tuple, Union
|
||||||
|
from .utils.vessel_parser import get_vessel, find_solvent_vessel
|
||||||
|
from .utils.unit_parser import parse_volume_input
|
||||||
from .pump_protocol import generate_pump_protocol_with_rinsing
|
from .pump_protocol import generate_pump_protocol_with_rinsing
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def debug_print(message):
|
def debug_print(message):
|
||||||
"""调试输出"""
|
"""调试输出"""
|
||||||
print(f"💎 [RECRYSTALLIZE] {message}", flush=True)
|
|
||||||
logger.info(f"[RECRYSTALLIZE] {message}")
|
logger.info(f"[RECRYSTALLIZE] {message}")
|
||||||
|
|
||||||
|
|
||||||
def parse_volume_with_units(volume_input: Union[str, float, int], default_unit: str = "mL") -> float:
|
|
||||||
"""
|
|
||||||
解析带单位的体积输入
|
|
||||||
|
|
||||||
Args:
|
|
||||||
volume_input: 体积输入(如 "100 mL", "2.5 L", "500", "?", 100.0)
|
|
||||||
default_unit: 默认单位(默认为毫升)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: 体积(毫升)
|
|
||||||
"""
|
|
||||||
if not volume_input:
|
|
||||||
debug_print("⚠️ 体积输入为空,返回 0.0mL 📦")
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
# 处理数值输入
|
|
||||||
if isinstance(volume_input, (int, float)):
|
|
||||||
result = float(volume_input)
|
|
||||||
debug_print(f"🔢 数值体积输入: {volume_input} → {result}mL(默认单位)💧")
|
|
||||||
return result
|
|
||||||
|
|
||||||
# 处理字符串输入
|
|
||||||
volume_str = str(volume_input).lower().strip()
|
|
||||||
debug_print(f"🔍 解析体积字符串: '{volume_str}' 📝")
|
|
||||||
|
|
||||||
# 处理特殊值
|
|
||||||
if volume_str in ['?', 'unknown', 'tbd', 'to be determined']:
|
|
||||||
default_volume = 50.0 # 50mL默认值
|
|
||||||
debug_print(f"❓ 检测到未知体积,使用默认值: {default_volume}mL 🎯")
|
|
||||||
return default_volume
|
|
||||||
|
|
||||||
# 如果是纯数字,使用默认单位
|
|
||||||
try:
|
|
||||||
value = float(volume_str)
|
|
||||||
if default_unit.lower() in ["ml", "milliliter"]:
|
|
||||||
result = value
|
|
||||||
elif default_unit.lower() in ["l", "liter"]:
|
|
||||||
result = value * 1000.0
|
|
||||||
elif default_unit.lower() in ["μl", "ul", "microliter"]:
|
|
||||||
result = value / 1000.0
|
|
||||||
else:
|
|
||||||
result = value # 默认mL
|
|
||||||
debug_print(f"🔢 纯数字输入: {volume_str} → {result}mL(单位: {default_unit})📏")
|
|
||||||
return result
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 移除空格并提取数字和单位
|
|
||||||
volume_clean = re.sub(r'\s+', '', volume_str)
|
|
||||||
|
|
||||||
# 匹配数字和单位的正则表达式
|
|
||||||
match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean)
|
|
||||||
|
|
||||||
if not match:
|
|
||||||
debug_print(f"⚠️ 无法解析体积: '{volume_str}',使用默认值: 50mL 🎯")
|
|
||||||
return 50.0
|
|
||||||
|
|
||||||
value = float(match.group(1))
|
|
||||||
unit = match.group(2) or default_unit.lower()
|
|
||||||
|
|
||||||
# 转换为毫升
|
|
||||||
if unit in ['l', 'liter']:
|
|
||||||
volume = value * 1000.0 # L -> mL
|
|
||||||
debug_print(f"📏 升转毫升: {value}L → {volume}mL 💧")
|
|
||||||
elif unit in ['μl', 'ul', 'microliter']:
|
|
||||||
volume = value / 1000.0 # μL -> mL
|
|
||||||
debug_print(f"📏 微升转毫升: {value}μL → {volume}mL 💧")
|
|
||||||
else: # ml, milliliter 或默认
|
|
||||||
volume = value # 已经是mL
|
|
||||||
debug_print(f"📏 毫升单位: {value}mL → {volume}mL 💧")
|
|
||||||
|
|
||||||
debug_print(f"✅ 体积解析完成: '{volume_str}' → {volume}mL ✨")
|
|
||||||
return volume
|
|
||||||
|
|
||||||
|
|
||||||
def parse_ratio(ratio_str: str) -> Tuple[float, float]:
|
def parse_ratio(ratio_str: str) -> Tuple[float, float]:
|
||||||
"""
|
"""
|
||||||
解析比例字符串,支持多种格式
|
解析比例字符串,支持多种格式
|
||||||
@@ -136,131 +62,6 @@ def parse_ratio(ratio_str: str) -> Tuple[float, float]:
|
|||||||
return 1.0, 1.0
|
return 1.0, 1.0
|
||||||
|
|
||||||
|
|
||||||
def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
|
|
||||||
"""
|
|
||||||
查找溶剂容器
|
|
||||||
|
|
||||||
Args:
|
|
||||||
G: 网络图
|
|
||||||
solvent: 溶剂名称
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 溶剂容器ID
|
|
||||||
"""
|
|
||||||
debug_print(f"🔍 正在查找溶剂 '{solvent}' 的容器... 🧪")
|
|
||||||
|
|
||||||
# 构建可能的容器名称
|
|
||||||
possible_names = [
|
|
||||||
f"flask_{solvent}",
|
|
||||||
f"bottle_{solvent}",
|
|
||||||
f"reagent_{solvent}",
|
|
||||||
f"reagent_bottle_{solvent}",
|
|
||||||
f"{solvent}_flask",
|
|
||||||
f"{solvent}_bottle",
|
|
||||||
f"{solvent}",
|
|
||||||
f"vessel_{solvent}",
|
|
||||||
]
|
|
||||||
|
|
||||||
debug_print(f"📋 候选容器名称: {possible_names[:3]}... (共{len(possible_names)}个) 📝")
|
|
||||||
|
|
||||||
# 第一步:通过容器名称匹配
|
|
||||||
debug_print(" 🎯 步骤1: 精确名称匹配...")
|
|
||||||
for vessel_name in possible_names:
|
|
||||||
if vessel_name in G.nodes():
|
|
||||||
debug_print(f" 🎉 通过名称匹配找到容器: {vessel_name} ✨")
|
|
||||||
return vessel_name
|
|
||||||
|
|
||||||
# 第二步:通过模糊匹配(节点ID和名称)
|
|
||||||
debug_print(" 🔍 步骤2: 模糊名称匹配...")
|
|
||||||
for node_id in G.nodes():
|
|
||||||
if G.nodes[node_id].get('type') == 'container':
|
|
||||||
node_name = G.nodes[node_id].get('name', '').lower()
|
|
||||||
|
|
||||||
if solvent.lower() in node_id.lower() or solvent.lower() in node_name:
|
|
||||||
debug_print(f" 🎉 通过模糊匹配找到容器: {node_id} (名称: {node_name}) ✨")
|
|
||||||
return node_id
|
|
||||||
|
|
||||||
# 第三步:通过配置中的试剂信息匹配
|
|
||||||
debug_print(" 🧪 步骤3: 配置试剂信息匹配...")
|
|
||||||
for node_id in G.nodes():
|
|
||||||
if G.nodes[node_id].get('type') == 'container':
|
|
||||||
# 检查 config 中的 reagent 字段
|
|
||||||
node_config = G.nodes[node_id].get('config', {})
|
|
||||||
config_reagent = node_config.get('reagent', '').lower()
|
|
||||||
|
|
||||||
if config_reagent and solvent.lower() == config_reagent:
|
|
||||||
debug_print(f" 🎉 通过config.reagent匹配找到容器: {node_id} (试剂: {config_reagent}) ✨")
|
|
||||||
return node_id
|
|
||||||
|
|
||||||
# 第四步:通过数据中的试剂信息匹配
|
|
||||||
debug_print(" 🧪 步骤4: 数据试剂信息匹配...")
|
|
||||||
for node_id in G.nodes():
|
|
||||||
if G.nodes[node_id].get('type') == 'container':
|
|
||||||
vessel_data = G.nodes[node_id].get('data', {})
|
|
||||||
|
|
||||||
# 检查 data 中的 reagent_name 字段
|
|
||||||
reagent_name = vessel_data.get('reagent_name', '').lower()
|
|
||||||
if reagent_name and solvent.lower() == reagent_name:
|
|
||||||
debug_print(f" 🎉 通过data.reagent_name匹配找到容器: {node_id} (试剂: {reagent_name}) ✨")
|
|
||||||
return node_id
|
|
||||||
|
|
||||||
# 检查 data 中的液体信息
|
|
||||||
liquids = vessel_data.get('liquid', [])
|
|
||||||
for liquid in liquids:
|
|
||||||
if isinstance(liquid, dict):
|
|
||||||
liquid_type = (liquid.get('liquid_type') or liquid.get('name', '')).lower()
|
|
||||||
|
|
||||||
if solvent.lower() in liquid_type:
|
|
||||||
debug_print(f" 🎉 通过液体类型匹配找到容器: {node_id} (液体类型: {liquid_type}) ✨")
|
|
||||||
return node_id
|
|
||||||
|
|
||||||
# 第五步:部分匹配(如果前面都没找到)
|
|
||||||
debug_print(" 🔍 步骤5: 部分匹配...")
|
|
||||||
for node_id in G.nodes():
|
|
||||||
if G.nodes[node_id].get('type') == 'container':
|
|
||||||
node_config = G.nodes[node_id].get('config', {})
|
|
||||||
node_data = G.nodes[node_id].get('data', {})
|
|
||||||
node_name = G.nodes[node_id].get('name', '').lower()
|
|
||||||
|
|
||||||
config_reagent = node_config.get('reagent', '').lower()
|
|
||||||
data_reagent = node_data.get('reagent_name', '').lower()
|
|
||||||
|
|
||||||
# 检查是否包含溶剂名称
|
|
||||||
if (solvent.lower() in config_reagent or
|
|
||||||
solvent.lower() in data_reagent or
|
|
||||||
solvent.lower() in node_name or
|
|
||||||
solvent.lower() in node_id.lower()):
|
|
||||||
debug_print(f" 🎉 通过部分匹配找到容器: {node_id} ✨")
|
|
||||||
debug_print(f" - 节点名称: {node_name}")
|
|
||||||
debug_print(f" - 配置试剂: {config_reagent}")
|
|
||||||
debug_print(f" - 数据试剂: {data_reagent}")
|
|
||||||
return node_id
|
|
||||||
|
|
||||||
# 调试信息:列出所有容器
|
|
||||||
debug_print(" 🔎 调试信息:列出所有容器...")
|
|
||||||
container_list = []
|
|
||||||
for node_id in G.nodes():
|
|
||||||
if G.nodes[node_id].get('type') == 'container':
|
|
||||||
node_config = G.nodes[node_id].get('config', {})
|
|
||||||
node_data = G.nodes[node_id].get('data', {})
|
|
||||||
node_name = G.nodes[node_id].get('name', '')
|
|
||||||
|
|
||||||
container_info = {
|
|
||||||
'id': node_id,
|
|
||||||
'name': node_name,
|
|
||||||
'config_reagent': node_config.get('reagent', ''),
|
|
||||||
'data_reagent': node_data.get('reagent_name', '')
|
|
||||||
}
|
|
||||||
container_list.append(container_info)
|
|
||||||
debug_print(f" - 容器: {node_id}, 名称: {node_name}, config试剂: {node_config.get('reagent', '')}, data试剂: {node_data.get('reagent_name', '')}")
|
|
||||||
|
|
||||||
debug_print(f"❌ 找不到溶剂 '{solvent}' 对应的容器 😭")
|
|
||||||
debug_print(f"🔍 查找的溶剂: '{solvent}' (小写: '{solvent.lower()}')")
|
|
||||||
debug_print(f"📊 总共发现 {len(container_list)} 个容器")
|
|
||||||
|
|
||||||
raise ValueError(f"找不到溶剂 '{solvent}' 对应的容器")
|
|
||||||
|
|
||||||
|
|
||||||
def generate_recrystallize_protocol(
|
def generate_recrystallize_protocol(
|
||||||
G: nx.DiGraph,
|
G: nx.DiGraph,
|
||||||
vessel: dict, # 🔧 修改:从字符串改为字典类型
|
vessel: dict, # 🔧 修改:从字符串改为字典类型
|
||||||
@@ -287,16 +88,7 @@ def generate_recrystallize_protocol(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# 🔧 核心修改:从字典中提取容器ID
|
# 🔧 核心修改:从字典中提取容器ID
|
||||||
# 统一处理vessel参数
|
vessel_id, vessel_data = get_vessel(vessel)
|
||||||
if isinstance(vessel, dict):
|
|
||||||
if "id" not in vessel:
|
|
||||||
vessel_id = list(vessel.values())[0].get("id", "")
|
|
||||||
else:
|
|
||||||
vessel_id = vessel.get("id", "")
|
|
||||||
vessel_data = vessel.get("data", {})
|
|
||||||
else:
|
|
||||||
vessel_id = str(vessel)
|
|
||||||
vessel_data = G.nodes[vessel_id].get("data", {}) if vessel_id in G.nodes() else {}
|
|
||||||
|
|
||||||
action_sequence = []
|
action_sequence = []
|
||||||
|
|
||||||
@@ -330,7 +122,7 @@ def generate_recrystallize_protocol(
|
|||||||
|
|
||||||
# 2. 解析体积(支持单位)
|
# 2. 解析体积(支持单位)
|
||||||
debug_print("📍 步骤2: 解析体积(支持单位)... 💧")
|
debug_print("📍 步骤2: 解析体积(支持单位)... 💧")
|
||||||
final_volume = parse_volume_with_units(volume, "mL")
|
final_volume = parse_volume_input(volume, "mL")
|
||||||
debug_print(f"🎯 体积解析完成: {volume} → {final_volume}mL ✨")
|
debug_print(f"🎯 体积解析完成: {volume} → {final_volume}mL ✨")
|
||||||
|
|
||||||
# 3. 解析比例
|
# 3. 解析比例
|
||||||
@@ -582,7 +374,7 @@ def test_recrystallize_protocol():
|
|||||||
debug_print("💧 测试体积解析...")
|
debug_print("💧 测试体积解析...")
|
||||||
test_volumes = ["100 mL", "2.5 L", "500", "50.5", "?", "invalid"]
|
test_volumes = ["100 mL", "2.5 L", "500", "50.5", "?", "invalid"]
|
||||||
for vol in test_volumes:
|
for vol in test_volumes:
|
||||||
parsed = parse_volume_with_units(vol)
|
parsed = parse_volume_input(vol)
|
||||||
debug_print(f" 📊 体积 '{vol}' -> {parsed}mL")
|
debug_print(f" 📊 体积 '{vol}' -> {parsed}mL")
|
||||||
|
|
||||||
# 测试比例解析
|
# 测试比例解析
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
def debug_print(message):
|
def debug_print(message):
|
||||||
"""调试输出"""
|
"""调试输出"""
|
||||||
print(f"🏛️ [RUN_COLUMN] {message}", flush=True)
|
|
||||||
logger.info(f"[RUN_COLUMN] {message}")
|
logger.info(f"[RUN_COLUMN] {message}")
|
||||||
|
|
||||||
def parse_percentage(pct_str: str) -> float:
|
def parse_percentage(pct_str: str) -> float:
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
from functools import partial
|
||||||
|
|
||||||
import networkx as nx
|
import networkx as nx
|
||||||
import re
|
import re
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from typing import List, Dict, Any, Union
|
from typing import List, Dict, Any, Union
|
||||||
|
from .utils.vessel_parser import get_vessel
|
||||||
|
from .utils.logger_util import action_log
|
||||||
from .pump_protocol import generate_pump_protocol_with_rinsing
|
from .pump_protocol import generate_pump_protocol_with_rinsing
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -20,48 +24,472 @@ def debug_print(message):
|
|||||||
try:
|
try:
|
||||||
# 确保消息是字符串格式
|
# 确保消息是字符串格式
|
||||||
safe_message = str(message)
|
safe_message = str(message)
|
||||||
print(f"🌀 [SEPARATE] {safe_message}", flush=True)
|
|
||||||
logger.info(f"[SEPARATE] {safe_message}")
|
logger.info(f"[SEPARATE] {safe_message}")
|
||||||
except UnicodeEncodeError:
|
except UnicodeEncodeError:
|
||||||
# 如果编码失败,尝试替换不支持的字符
|
# 如果编码失败,尝试替换不支持的字符
|
||||||
safe_message = str(message).encode('utf-8', errors='replace').decode('utf-8')
|
safe_message = str(message).encode('utf-8', errors='replace').decode('utf-8')
|
||||||
print(f"🌀 [SEPARATE] {safe_message}", flush=True)
|
|
||||||
logger.info(f"[SEPARATE] {safe_message}")
|
logger.info(f"[SEPARATE] {safe_message}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 最后的安全措施
|
# 最后的安全措施
|
||||||
fallback_message = f"日志输出错误: {repr(message)}"
|
fallback_message = f"日志输出错误: {repr(message)}"
|
||||||
print(f"🌀 [SEPARATE] {fallback_message}", flush=True)
|
|
||||||
logger.info(f"[SEPARATE] {fallback_message}")
|
logger.info(f"[SEPARATE] {fallback_message}")
|
||||||
|
|
||||||
def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]:
|
create_action_log = partial(action_log, prefix="[SEPARATE]")
|
||||||
"""创建一个动作日志 - 支持中文和emoji"""
|
|
||||||
|
|
||||||
|
def generate_separate_protocol(
|
||||||
|
G: nx.DiGraph,
|
||||||
|
# 🔧 基础参数,支持XDL的vessel参数
|
||||||
|
vessel: dict = None, # 🔧 修改:从字符串改为字典类型
|
||||||
|
purpose: str = "separate", # 分离目的
|
||||||
|
product_phase: str = "top", # 产物相
|
||||||
|
# 🔧 可选的详细参数
|
||||||
|
from_vessel: Union[str, dict] = "", # 源容器(通常在separate前已经transfer了)
|
||||||
|
separation_vessel: Union[str, dict] = "", # 分离容器(与vessel同义)
|
||||||
|
to_vessel: Union[str, dict] = "", # 目标容器(可选)
|
||||||
|
waste_phase_to_vessel: Union[str, dict] = "", # 废相目标容器
|
||||||
|
product_vessel: Union[str, dict] = "", # XDL: 产物容器(与to_vessel同义)
|
||||||
|
waste_vessel: Union[str, dict] = "", # XDL: 废液容器(与waste_phase_to_vessel同义)
|
||||||
|
# 🔧 溶剂相关参数
|
||||||
|
solvent: str = "", # 溶剂名称
|
||||||
|
solvent_volume: Union[str, float] = 0.0, # 溶剂体积
|
||||||
|
volume: Union[str, float] = 0.0, # XDL: 体积(与solvent_volume同义)
|
||||||
|
# 🔧 操作参数
|
||||||
|
through: str = "", # 通过材料
|
||||||
|
repeats: int = 1, # 重复次数
|
||||||
|
stir_time: float = 30.0, # 搅拌时间(秒)
|
||||||
|
stir_speed: float = 300.0, # 搅拌速度
|
||||||
|
settling_time: float = 300.0, # 沉降时间(秒)
|
||||||
|
**kwargs
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
生成分离操作的协议序列 - 支持vessel字典和体积运算
|
||||||
|
|
||||||
|
支持XDL参数格式:
|
||||||
|
- vessel: 分离容器字典(必需)
|
||||||
|
- purpose: "wash", "extract", "separate"
|
||||||
|
- product_phase: "top", "bottom"
|
||||||
|
- product_vessel: 产物收集容器
|
||||||
|
- waste_vessel: 废液收集容器
|
||||||
|
- solvent: 溶剂名称
|
||||||
|
- volume: "200 mL", "?" 或数值
|
||||||
|
- repeats: 重复次数
|
||||||
|
|
||||||
|
分离流程:
|
||||||
|
1. (可选)添加溶剂到分离容器
|
||||||
|
2. 搅拌混合
|
||||||
|
3. 静置分层
|
||||||
|
4. 收集指定相到目标容器
|
||||||
|
5. 重复指定次数
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 🔧 核心修改:vessel参数兼容处理
|
||||||
|
if vessel is None:
|
||||||
|
if isinstance(separation_vessel, dict):
|
||||||
|
vessel = separation_vessel
|
||||||
|
else:
|
||||||
|
raise ValueError("必须提供vessel字典参数")
|
||||||
|
|
||||||
|
# 🔧 核心修改:从字典中提取容器ID
|
||||||
|
vessel_id, vessel_data = get_vessel(vessel)
|
||||||
|
|
||||||
|
debug_print("🌀" * 20)
|
||||||
|
debug_print("🚀 开始生成分离协议(支持vessel字典和体积运算)✨")
|
||||||
|
debug_print(f"📝 输入参数:")
|
||||||
|
debug_print(f" 🥽 vessel: {vessel} (ID: {vessel_id})")
|
||||||
|
debug_print(f" 🎯 分离目的: '{purpose}'")
|
||||||
|
debug_print(f" 📊 产物相: '{product_phase}'")
|
||||||
|
debug_print(f" 💧 溶剂: '{solvent}'")
|
||||||
|
debug_print(f" 📏 体积: {volume} (类型: {type(volume)})")
|
||||||
|
debug_print(f" 🔄 重复次数: {repeats}")
|
||||||
|
debug_print(f" 🎯 产物容器: '{product_vessel}'")
|
||||||
|
debug_print(f" 🗑️ 废液容器: '{waste_vessel}'")
|
||||||
|
debug_print(f" 📦 其他参数: {kwargs}")
|
||||||
|
debug_print("🌀" * 20)
|
||||||
|
|
||||||
|
action_sequence = []
|
||||||
|
|
||||||
|
# 🔧 新增:记录分离前的容器状态
|
||||||
|
debug_print("🔍 记录分离前容器状态...")
|
||||||
|
original_liquid_volume = get_vessel_liquid_volume(vessel)
|
||||||
|
debug_print(f"📊 分离前液体体积: {original_liquid_volume:.2f}mL")
|
||||||
|
|
||||||
|
# === 参数验证和标准化 ===
|
||||||
|
debug_print("🔍 步骤1: 参数验证和标准化...")
|
||||||
|
action_sequence.append(create_action_log(f"开始分离操作 - 容器: {vessel_id}", "🎬"))
|
||||||
|
action_sequence.append(create_action_log(f"分离目的: {purpose}", "🧪"))
|
||||||
|
action_sequence.append(create_action_log(f"产物相: {product_phase}", "📊"))
|
||||||
|
|
||||||
|
# 统一容器参数 - 支持字典和字符串
|
||||||
|
def extract_vessel_id(vessel_param):
|
||||||
|
if isinstance(vessel_param, dict):
|
||||||
|
return vessel_param.get("id", "")
|
||||||
|
elif isinstance(vessel_param, str):
|
||||||
|
return vessel_param
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
final_vessel_id, _ = vessel_id
|
||||||
|
final_to_vessel_id, _ = get_vessel(to_vessel) or get_vessel(product_vessel)
|
||||||
|
final_waste_vessel_id, _ = get_vessel(waste_phase_to_vessel) or get_vessel(waste_vessel)
|
||||||
|
|
||||||
|
# 统一体积参数
|
||||||
|
final_volume = parse_volume_input(volume or solvent_volume)
|
||||||
|
|
||||||
|
# 🔧 修复:确保repeats至少为1
|
||||||
|
if repeats <= 0:
|
||||||
|
repeats = 1
|
||||||
|
debug_print(f"⚠️ 重复次数参数 <= 0,自动设置为 1")
|
||||||
|
|
||||||
|
debug_print(f"🔧 标准化后的参数:")
|
||||||
|
debug_print(f" 🥼 分离容器: '{final_vessel_id}'")
|
||||||
|
debug_print(f" 🎯 产物容器: '{final_to_vessel_id}'")
|
||||||
|
debug_print(f" 🗑️ 废液容器: '{final_waste_vessel_id}'")
|
||||||
|
debug_print(f" 📏 溶剂体积: {final_volume}mL")
|
||||||
|
debug_print(f" 🔄 重复次数: {repeats}")
|
||||||
|
|
||||||
|
action_sequence.append(create_action_log(f"分离容器: {final_vessel_id}", "🧪"))
|
||||||
|
action_sequence.append(create_action_log(f"溶剂体积: {final_volume}mL", "📏"))
|
||||||
|
action_sequence.append(create_action_log(f"重复次数: {repeats}", "🔄"))
|
||||||
|
|
||||||
|
# 验证必需参数
|
||||||
|
if not purpose:
|
||||||
|
purpose = "separate"
|
||||||
|
if not product_phase:
|
||||||
|
product_phase = "top"
|
||||||
|
if purpose not in ["wash", "extract", "separate"]:
|
||||||
|
debug_print(f"⚠️ 未知的分离目的 '{purpose}',使用默认值 'separate'")
|
||||||
|
purpose = "separate"
|
||||||
|
action_sequence.append(create_action_log(f"未知目的,使用: {purpose}", "⚠️"))
|
||||||
|
if product_phase not in ["top", "bottom"]:
|
||||||
|
debug_print(f"⚠️ 未知的产物相 '{product_phase}',使用默认值 'top'")
|
||||||
|
product_phase = "top"
|
||||||
|
action_sequence.append(create_action_log(f"未知相别,使用: {product_phase}", "⚠️"))
|
||||||
|
|
||||||
|
debug_print("✅ 参数验证通过")
|
||||||
|
action_sequence.append(create_action_log("参数验证通过", "✅"))
|
||||||
|
|
||||||
|
# === 查找设备 ===
|
||||||
|
debug_print("🔍 步骤2: 查找设备...")
|
||||||
|
action_sequence.append(create_action_log("正在查找相关设备...", "🔍"))
|
||||||
|
|
||||||
|
# 查找分离器设备
|
||||||
|
separator_device = find_separator_device(G, final_vessel_id) # 🔧 使用 final_vessel_id
|
||||||
|
if separator_device:
|
||||||
|
action_sequence.append(create_action_log(f"找到分离器设备: {separator_device}", "🧪"))
|
||||||
|
else:
|
||||||
|
debug_print("⚠️ 未找到分离器设备,可能无法执行分离")
|
||||||
|
action_sequence.append(create_action_log("未找到分离器设备", "⚠️"))
|
||||||
|
|
||||||
|
# 查找搅拌器
|
||||||
|
stirrer_device = find_connected_stirrer(G, final_vessel_id) # 🔧 使用 final_vessel_id
|
||||||
|
if stirrer_device:
|
||||||
|
action_sequence.append(create_action_log(f"找到搅拌器: {stirrer_device}", "🌪️"))
|
||||||
|
else:
|
||||||
|
action_sequence.append(create_action_log("未找到搅拌器", "⚠️"))
|
||||||
|
|
||||||
|
# 查找溶剂容器(如果需要)
|
||||||
|
solvent_vessel = ""
|
||||||
|
if solvent and solvent.strip():
|
||||||
|
solvent_vessel = find_solvent_vessel(G, solvent)
|
||||||
|
if solvent_vessel:
|
||||||
|
action_sequence.append(create_action_log(f"找到溶剂容器: {solvent_vessel}", "💧"))
|
||||||
|
else:
|
||||||
|
action_sequence.append(create_action_log(f"未找到溶剂容器: {solvent}", "⚠️"))
|
||||||
|
|
||||||
|
debug_print(f"📊 设备配置:")
|
||||||
|
debug_print(f" 🧪 分离器设备: '{separator_device}'")
|
||||||
|
debug_print(f" 🌪️ 搅拌器设备: '{stirrer_device}'")
|
||||||
|
debug_print(f" 💧 溶剂容器: '{solvent_vessel}'")
|
||||||
|
|
||||||
|
# === 执行分离流程 ===
|
||||||
|
debug_print("🔍 步骤3: 执行分离流程...")
|
||||||
|
action_sequence.append(create_action_log("开始分离工作流程", "🎯"))
|
||||||
|
|
||||||
|
# 🔧 新增:体积变化跟踪变量
|
||||||
|
current_volume = original_liquid_volume
|
||||||
|
|
||||||
try:
|
try:
|
||||||
full_message = f"{emoji} {message}"
|
for repeat_idx in range(repeats):
|
||||||
debug_print(full_message)
|
cycle_num = repeat_idx + 1
|
||||||
logger.info(full_message)
|
debug_print(f"🔄 第{cycle_num}轮: 开始分离循环 {cycle_num}/{repeats}")
|
||||||
|
action_sequence.append(create_action_log(f"分离循环 {cycle_num}/{repeats} 开始", "🔄"))
|
||||||
return {
|
|
||||||
"action_name": "wait",
|
# 步骤3.1: 添加溶剂(如果需要)
|
||||||
"action_kwargs": {
|
if solvent_vessel and final_volume > 0:
|
||||||
"time": 0.1,
|
debug_print(f"🔄 第{cycle_num}轮 步骤1: 添加溶剂 {solvent} ({final_volume}mL)")
|
||||||
"log_message": full_message,
|
action_sequence.append(create_action_log(f"向分离容器添加 {final_volume}mL {solvent}", "💧"))
|
||||||
"progress_message": full_message
|
|
||||||
}
|
try:
|
||||||
}
|
# 使用pump protocol添加溶剂
|
||||||
|
pump_actions = generate_pump_protocol_with_rinsing(
|
||||||
|
G=G,
|
||||||
|
from_vessel=solvent_vessel,
|
||||||
|
to_vessel=final_vessel_id, # 🔧 使用 final_vessel_id
|
||||||
|
volume=final_volume,
|
||||||
|
amount="",
|
||||||
|
time=0.0,
|
||||||
|
viscous=False,
|
||||||
|
rinsing_solvent="",
|
||||||
|
rinsing_volume=0.0,
|
||||||
|
rinsing_repeats=0,
|
||||||
|
solid=False,
|
||||||
|
flowrate=2.5,
|
||||||
|
transfer_flowrate=0.5,
|
||||||
|
rate_spec="",
|
||||||
|
event="",
|
||||||
|
through="",
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
action_sequence.extend(pump_actions)
|
||||||
|
debug_print(f"✅ 溶剂添加完成,添加了 {len(pump_actions)} 个动作")
|
||||||
|
action_sequence.append(create_action_log(f"溶剂转移完成 ({len(pump_actions)} 个操作)", "✅"))
|
||||||
|
|
||||||
|
# 🔧 新增:更新体积 - 添加溶剂后
|
||||||
|
current_volume += final_volume
|
||||||
|
update_vessel_volume(vessel, G, current_volume, f"添加{final_volume}mL {solvent}后")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
debug_print(f"❌ 溶剂添加失败: {str(e)}")
|
||||||
|
action_sequence.append(create_action_log(f"溶剂添加失败: {str(e)}", "❌"))
|
||||||
|
else:
|
||||||
|
debug_print(f"🔄 第{cycle_num}轮 步骤1: 无需添加溶剂")
|
||||||
|
action_sequence.append(create_action_log("无需添加溶剂", "⏭️"))
|
||||||
|
|
||||||
|
# 步骤3.2: 启动搅拌(如果有搅拌器)
|
||||||
|
if stirrer_device and stir_time > 0:
|
||||||
|
debug_print(f"🔄 第{cycle_num}轮 步骤2: 开始搅拌 ({stir_speed}rpm,持续 {stir_time}s)")
|
||||||
|
action_sequence.append(create_action_log(f"开始搅拌: {stir_speed}rpm,持续 {stir_time}s", "🌪️"))
|
||||||
|
|
||||||
|
action_sequence.append({
|
||||||
|
"device_id": stirrer_device,
|
||||||
|
"action_name": "start_stir",
|
||||||
|
"action_kwargs": {
|
||||||
|
"vessel": final_vessel_id, # 🔧 使用 final_vessel_id
|
||||||
|
"stir_speed": stir_speed,
|
||||||
|
"purpose": f"分离混合 - {purpose}"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# 搅拌等待
|
||||||
|
stir_minutes = stir_time / 60
|
||||||
|
action_sequence.append(create_action_log(f"搅拌中,持续 {stir_minutes:.1f} 分钟", "⏱️"))
|
||||||
|
action_sequence.append({
|
||||||
|
"action_name": "wait",
|
||||||
|
"action_kwargs": {"time": stir_time}
|
||||||
|
})
|
||||||
|
|
||||||
|
# 停止搅拌
|
||||||
|
action_sequence.append(create_action_log("停止搅拌器", "🛑"))
|
||||||
|
action_sequence.append({
|
||||||
|
"device_id": stirrer_device,
|
||||||
|
"action_name": "stop_stir",
|
||||||
|
"action_kwargs": {"vessel": final_vessel_id} # 🔧 使用 final_vessel_id
|
||||||
|
})
|
||||||
|
|
||||||
|
else:
|
||||||
|
debug_print(f"🔄 第{cycle_num}轮 步骤2: 无需搅拌")
|
||||||
|
action_sequence.append(create_action_log("无需搅拌", "⏭️"))
|
||||||
|
|
||||||
|
# 步骤3.3: 静置分层
|
||||||
|
if settling_time > 0:
|
||||||
|
debug_print(f"🔄 第{cycle_num}轮 步骤3: 静置分层 ({settling_time}s)")
|
||||||
|
settling_minutes = settling_time / 60
|
||||||
|
action_sequence.append(create_action_log(f"静置分层 ({settling_minutes:.1f} 分钟)", "⚖️"))
|
||||||
|
action_sequence.append({
|
||||||
|
"action_name": "wait",
|
||||||
|
"action_kwargs": {"time": settling_time}
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
debug_print(f"🔄 第{cycle_num}轮 步骤3: 未指定静置时间")
|
||||||
|
action_sequence.append(create_action_log("未指定静置时间", "⏭️"))
|
||||||
|
|
||||||
|
# 步骤3.4: 执行分离操作
|
||||||
|
if separator_device:
|
||||||
|
debug_print(f"🔄 第{cycle_num}轮 步骤4: 执行分离操作")
|
||||||
|
action_sequence.append(create_action_log(f"执行分离: 收集{product_phase}相", "🧪"))
|
||||||
|
|
||||||
|
# 🔧 替换为具体的分离操作逻辑(基于old版本)
|
||||||
|
|
||||||
|
# 首先进行分液判断(电导突跃)
|
||||||
|
action_sequence.append({
|
||||||
|
"device_id": separator_device,
|
||||||
|
"action_name": "valve_open",
|
||||||
|
"action_kwargs": {
|
||||||
|
"command": "delta > 0.05"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# 估算每相的体积(假设大致平分)
|
||||||
|
phase_volume = current_volume / 2
|
||||||
|
|
||||||
|
# 智能查找分离容器底部
|
||||||
|
separation_vessel_bottom = find_separation_vessel_bottom(G, final_vessel_id) # ✅
|
||||||
|
|
||||||
|
if product_phase == "bottom":
|
||||||
|
debug_print(f"🔄 收集底相产物到 {final_to_vessel_id}")
|
||||||
|
action_sequence.append(create_action_log("收集底相产物", "📦"))
|
||||||
|
|
||||||
|
# 产物转移到目标瓶
|
||||||
|
if final_to_vessel_id:
|
||||||
|
pump_actions = generate_pump_protocol_with_rinsing(
|
||||||
|
G=G,
|
||||||
|
from_vessel=separation_vessel_bottom,
|
||||||
|
to_vessel=final_to_vessel_id,
|
||||||
|
volume=current_volume,
|
||||||
|
flowrate=2.5,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
action_sequence.extend(pump_actions)
|
||||||
|
|
||||||
|
# 放出上面那一相,60秒后关阀门
|
||||||
|
action_sequence.append({
|
||||||
|
"device_id": separator_device,
|
||||||
|
"action_name": "valve_open",
|
||||||
|
"action_kwargs": {
|
||||||
|
"command": "time > 60"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# 弃去上面那一相进废液
|
||||||
|
if final_waste_vessel_id:
|
||||||
|
pump_actions = generate_pump_protocol_with_rinsing(
|
||||||
|
G=G,
|
||||||
|
from_vessel=separation_vessel_bottom,
|
||||||
|
to_vessel=final_waste_vessel_id,
|
||||||
|
volume=current_volume,
|
||||||
|
flowrate=2.5,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
action_sequence.extend(pump_actions)
|
||||||
|
|
||||||
|
elif product_phase == "top":
|
||||||
|
debug_print(f"🔄 收集上相产物到 {final_to_vessel_id}")
|
||||||
|
action_sequence.append(create_action_log("收集上相产物", "📦"))
|
||||||
|
|
||||||
|
# 弃去下面那一相进废液
|
||||||
|
if final_waste_vessel_id:
|
||||||
|
pump_actions = generate_pump_protocol_with_rinsing(
|
||||||
|
G=G,
|
||||||
|
from_vessel=separation_vessel_bottom,
|
||||||
|
to_vessel=final_waste_vessel_id,
|
||||||
|
volume=phase_volume,
|
||||||
|
flowrate=2.5,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
action_sequence.extend(pump_actions)
|
||||||
|
|
||||||
|
# 放出上面那一相,60秒后关阀门
|
||||||
|
action_sequence.append({
|
||||||
|
"device_id": separator_device,
|
||||||
|
"action_name": "valve_open",
|
||||||
|
"action_kwargs": {
|
||||||
|
"command": "time > 60"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# 产物转移到目标瓶
|
||||||
|
if final_to_vessel_id:
|
||||||
|
pump_actions = generate_pump_protocol_with_rinsing(
|
||||||
|
G=G,
|
||||||
|
from_vessel=separation_vessel_bottom,
|
||||||
|
to_vessel=final_to_vessel_id,
|
||||||
|
volume=phase_volume,
|
||||||
|
flowrate=2.5,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
action_sequence.extend(pump_actions)
|
||||||
|
|
||||||
|
debug_print(f"✅ 分离操作已完成")
|
||||||
|
action_sequence.append(create_action_log("分离操作完成", "✅"))
|
||||||
|
|
||||||
|
# 🔧 新增:分离后体积估算
|
||||||
|
separated_volume = phase_volume * 0.95 # 假设5%损失,只保留产物相体积
|
||||||
|
update_vessel_volume(vessel, G, separated_volume, f"分离操作后(第{cycle_num}轮)")
|
||||||
|
current_volume = separated_volume
|
||||||
|
|
||||||
|
# 收集结果
|
||||||
|
if final_to_vessel_id:
|
||||||
|
action_sequence.append(
|
||||||
|
create_action_log(f"产物 ({product_phase}相) 收集到: {final_to_vessel_id}", "📦"))
|
||||||
|
if final_waste_vessel_id:
|
||||||
|
action_sequence.append(create_action_log(f"废相收集到: {final_waste_vessel_id}", "🗑️"))
|
||||||
|
|
||||||
|
else:
|
||||||
|
debug_print(f"🔄 第{cycle_num}轮 步骤4: 无分离器设备,跳过分离")
|
||||||
|
action_sequence.append(create_action_log("无分离器设备可用", "❌"))
|
||||||
|
# 添加等待时间模拟分离
|
||||||
|
action_sequence.append({
|
||||||
|
"action_name": "wait",
|
||||||
|
"action_kwargs": {"time": 10.0}
|
||||||
|
})
|
||||||
|
|
||||||
|
# 🔧 新增:如果不是最后一次,从中转瓶转移回分液漏斗(基于old版本逻辑)
|
||||||
|
if repeat_idx < repeats - 1 and final_to_vessel_id and final_to_vessel_id != final_vessel_id:
|
||||||
|
debug_print(f"🔄 第{cycle_num}轮: 产物转移回分离容器准备下一轮")
|
||||||
|
action_sequence.append(create_action_log("产物转回分离容器,准备下一轮", "🔄"))
|
||||||
|
|
||||||
|
pump_actions = generate_pump_protocol_with_rinsing(
|
||||||
|
G=G,
|
||||||
|
from_vessel=final_to_vessel_id,
|
||||||
|
to_vessel=final_vessel_id,
|
||||||
|
volume=current_volume,
|
||||||
|
flowrate=2.5,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
action_sequence.extend(pump_actions)
|
||||||
|
|
||||||
|
# 更新体积回到分离容器
|
||||||
|
update_vessel_volume(vessel, G, current_volume, f"产物转回分离容器(第{cycle_num}轮后)")
|
||||||
|
|
||||||
|
# 循环间等待(除了最后一次)
|
||||||
|
if repeat_idx < repeats - 1:
|
||||||
|
debug_print(f"🔄 第{cycle_num}轮: 等待下一次循环...")
|
||||||
|
action_sequence.append(create_action_log("等待下一次循环...", "⏳"))
|
||||||
|
action_sequence.append({
|
||||||
|
"action_name": "wait",
|
||||||
|
"action_kwargs": {"time": 5}
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
action_sequence.append(create_action_log(f"分离循环 {cycle_num}/{repeats} 完成", "🌟"))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 如果emoji有问题,使用纯文本
|
debug_print(f"❌ 分离工作流程执行失败: {str(e)}")
|
||||||
safe_message = f"[日志] {message}"
|
action_sequence.append(create_action_log(f"分离工作流程失败: {str(e)}", "❌"))
|
||||||
debug_print(safe_message)
|
|
||||||
logger.info(safe_message)
|
# 🔧 新增:分离完成后的最终状态报告
|
||||||
|
final_liquid_volume = get_vessel_liquid_volume(vessel)
|
||||||
return {
|
|
||||||
"action_name": "wait",
|
# === 最终结果 ===
|
||||||
"action_kwargs": {
|
total_time = (stir_time + settling_time + 15) * repeats # 估算总时间
|
||||||
"time": 0.1,
|
|
||||||
"log_message": safe_message,
|
debug_print("🌀" * 20)
|
||||||
"progress_message": safe_message
|
debug_print(f"🎉 分离协议生成完成")
|
||||||
}
|
debug_print(f"📊 协议统计:")
|
||||||
}
|
debug_print(f" 📋 总动作数: {len(action_sequence)}")
|
||||||
|
debug_print(f" ⏱️ 预计总时间: {total_time:.0f}s ({total_time / 60:.1f} 分钟)")
|
||||||
|
debug_print(f" 🥼 分离容器: {final_vessel_id}")
|
||||||
|
debug_print(f" 🎯 分离目的: {purpose}")
|
||||||
|
debug_print(f" 📊 产物相: {product_phase}")
|
||||||
|
debug_print(f" 🔄 重复次数: {repeats}")
|
||||||
|
debug_print(f"💧 体积变化统计:")
|
||||||
|
debug_print(f" - 分离前体积: {original_liquid_volume:.2f}mL")
|
||||||
|
debug_print(f" - 分离后体积: {final_liquid_volume:.2f}mL")
|
||||||
|
if solvent:
|
||||||
|
debug_print(f" 💧 溶剂: {solvent} ({final_volume}mL × {repeats}轮 = {final_volume * repeats:.2f}mL)")
|
||||||
|
if final_to_vessel_id:
|
||||||
|
debug_print(f" 🎯 产物容器: {final_to_vessel_id}")
|
||||||
|
if final_waste_vessel_id:
|
||||||
|
debug_print(f" 🗑️ 废液容器: {final_waste_vessel_id}")
|
||||||
|
debug_print("🌀" * 20)
|
||||||
|
|
||||||
|
# 添加完成日志
|
||||||
|
summary_msg = f"分离协议完成: {final_vessel_id} ({purpose},{repeats} 次循环)"
|
||||||
|
if solvent:
|
||||||
|
summary_msg += f",使用 {final_volume * repeats:.2f}mL {solvent}"
|
||||||
|
action_sequence.append(create_action_log(summary_msg, "🎉"))
|
||||||
|
|
||||||
|
return action_sequence
|
||||||
|
|
||||||
def parse_volume_input(volume_input: Union[str, float]) -> float:
|
def parse_volume_input(volume_input: Union[str, float]) -> float:
|
||||||
"""
|
"""
|
||||||
@@ -364,386 +792,54 @@ def update_vessel_volume(vessel: dict, G: nx.DiGraph, new_volume: float, descrip
|
|||||||
|
|
||||||
debug_print(f"📊 容器 '{vessel_id}' 体积已更新为: {new_volume:.2f}mL")
|
debug_print(f"📊 容器 '{vessel_id}' 体积已更新为: {new_volume:.2f}mL")
|
||||||
|
|
||||||
def generate_separate_protocol(
|
|
||||||
G: nx.DiGraph,
|
def find_separation_vessel_bottom(G: nx.DiGraph, vessel_id: str) -> str:
|
||||||
# 🔧 基础参数,支持XDL的vessel参数
|
"""
|
||||||
vessel: dict = None, # 🔧 修改:从字符串改为字典类型
|
智能查找分离容器的底部容器(假设为flask或vessel类型)
|
||||||
purpose: str = "separate", # 分离目的
|
|
||||||
product_phase: str = "top", # 产物相
|
Args:
|
||||||
# 🔧 可选的详细参数
|
G: 网络图
|
||||||
from_vessel: Union[str, dict] = "", # 源容器(通常在separate前已经transfer了)
|
vessel_id: 分离容器ID
|
||||||
separation_vessel: Union[str, dict] = "", # 分离容器(与vessel同义)
|
|
||||||
to_vessel: Union[str, dict] = "", # 目标容器(可选)
|
Returns:
|
||||||
waste_phase_to_vessel: Union[str, dict] = "", # 废相目标容器
|
str: 底部容器ID
|
||||||
product_vessel: Union[str, dict] = "", # XDL: 产物容器(与to_vessel同义)
|
"""
|
||||||
waste_vessel: Union[str, dict] = "", # XDL: 废液容器(与waste_phase_to_vessel同义)
|
debug_print(f"🔍 查找分离容器 {vessel_id} 的底部容器...")
|
||||||
# 🔧 溶剂相关参数
|
|
||||||
solvent: str = "", # 溶剂名称
|
# 方法1:根据命名规则推测
|
||||||
solvent_volume: Union[str, float] = 0.0, # 溶剂体积
|
possible_bottoms = [
|
||||||
volume: Union[str, float] = 0.0, # XDL: 体积(与solvent_volume同义)
|
f"{vessel_id}_bottom",
|
||||||
# 🔧 操作参数
|
f"flask_{vessel_id}",
|
||||||
through: str = "", # 通过材料
|
f"vessel_{vessel_id}",
|
||||||
repeats: int = 1, # 重复次数
|
f"{vessel_id}_flask",
|
||||||
stir_time: float = 30.0, # 搅拌时间(秒)
|
f"{vessel_id}_vessel"
|
||||||
stir_speed: float = 300.0, # 搅拌速度
|
]
|
||||||
settling_time: float = 300.0, # 沉降时间(秒)
|
|
||||||
**kwargs
|
debug_print(f"📋 尝试的底部容器名称: {possible_bottoms}")
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
for bottom_id in possible_bottoms:
|
||||||
生成分离操作的协议序列 - 支持vessel字典和体积运算
|
if bottom_id in G.nodes():
|
||||||
|
node_type = G.nodes[bottom_id].get('type', '')
|
||||||
支持XDL参数格式:
|
if node_type == 'container':
|
||||||
- vessel: 分离容器字典(必需)
|
debug_print(f"✅ 通过命名规则找到底部容器: {bottom_id}")
|
||||||
- purpose: "wash", "extract", "separate"
|
return bottom_id
|
||||||
- product_phase: "top", "bottom"
|
|
||||||
- product_vessel: 产物收集容器
|
# 方法2:查找与分离器相连的容器(假设底部容器会与分离器相连)
|
||||||
- waste_vessel: 废液收集容器
|
debug_print(f"📋 方法2: 查找连接的容器...")
|
||||||
- solvent: 溶剂名称
|
for node in G.nodes():
|
||||||
- volume: "200 mL", "?" 或数值
|
node_data = G.nodes[node]
|
||||||
- repeats: 重复次数
|
node_class = node_data.get('class', '') or ''
|
||||||
|
|
||||||
分离流程:
|
if 'separator' in node_class.lower():
|
||||||
1. (可选)添加溶剂到分离容器
|
# 检查分离器的输入端
|
||||||
2. 搅拌混合
|
if G.has_edge(node, vessel_id):
|
||||||
3. 静置分层
|
for neighbor in G.neighbors(node):
|
||||||
4. 收集指定相到目标容器
|
if neighbor != vessel_id:
|
||||||
5. 重复指定次数
|
neighbor_type = G.nodes[neighbor].get('type', '')
|
||||||
"""
|
if neighbor_type == 'container':
|
||||||
|
debug_print(f"✅ 通过连接找到底部容器: {neighbor}")
|
||||||
# 🔧 核心修改:vessel参数兼容处理
|
return neighbor
|
||||||
if vessel is None:
|
|
||||||
if isinstance(separation_vessel, dict):
|
debug_print(f"❌ 无法找到分离容器 {vessel_id} 的底部容器")
|
||||||
vessel = separation_vessel
|
return ""
|
||||||
else:
|
|
||||||
raise ValueError("必须提供vessel字典参数")
|
|
||||||
|
|
||||||
# 🔧 核心修改:从字典中提取容器ID
|
|
||||||
# 统一处理vessel参数
|
|
||||||
if isinstance(vessel, dict):
|
|
||||||
if "id" not in vessel:
|
|
||||||
vessel_id = list(vessel.values())[0].get("id", "")
|
|
||||||
else:
|
|
||||||
vessel_id = vessel.get("id", "")
|
|
||||||
vessel_data = vessel.get("data", {})
|
|
||||||
else:
|
|
||||||
vessel_id = str(vessel)
|
|
||||||
vessel_data = G.nodes[vessel_id].get("data", {}) if vessel_id in G.nodes() else {}
|
|
||||||
|
|
||||||
debug_print("🌀" * 20)
|
|
||||||
debug_print("🚀 开始生成分离协议(支持vessel字典和体积运算)✨")
|
|
||||||
debug_print(f"📝 输入参数:")
|
|
||||||
debug_print(f" 🥽 vessel: {vessel} (ID: {vessel_id})")
|
|
||||||
debug_print(f" 🎯 分离目的: '{purpose}'")
|
|
||||||
debug_print(f" 📊 产物相: '{product_phase}'")
|
|
||||||
debug_print(f" 💧 溶剂: '{solvent}'")
|
|
||||||
debug_print(f" 📏 体积: {volume} (类型: {type(volume)})")
|
|
||||||
debug_print(f" 🔄 重复次数: {repeats}")
|
|
||||||
debug_print(f" 🎯 产物容器: '{product_vessel}'")
|
|
||||||
debug_print(f" 🗑️ 废液容器: '{waste_vessel}'")
|
|
||||||
debug_print(f" 📦 其他参数: {kwargs}")
|
|
||||||
debug_print("🌀" * 20)
|
|
||||||
|
|
||||||
action_sequence = []
|
|
||||||
|
|
||||||
# 🔧 新增:记录分离前的容器状态
|
|
||||||
debug_print("🔍 记录分离前容器状态...")
|
|
||||||
original_liquid_volume = get_vessel_liquid_volume(vessel)
|
|
||||||
debug_print(f"📊 分离前液体体积: {original_liquid_volume:.2f}mL")
|
|
||||||
|
|
||||||
# === 参数验证和标准化 ===
|
|
||||||
debug_print("🔍 步骤1: 参数验证和标准化...")
|
|
||||||
action_sequence.append(create_action_log(f"开始分离操作 - 容器: {vessel_id}", "🎬"))
|
|
||||||
action_sequence.append(create_action_log(f"分离目的: {purpose}", "🧪"))
|
|
||||||
action_sequence.append(create_action_log(f"产物相: {product_phase}", "📊"))
|
|
||||||
|
|
||||||
# 统一容器参数 - 支持字典和字符串
|
|
||||||
def extract_vessel_id(vessel_param):
|
|
||||||
if isinstance(vessel_param, dict):
|
|
||||||
return vessel_param.get("id", "")
|
|
||||||
elif isinstance(vessel_param, str):
|
|
||||||
return vessel_param
|
|
||||||
else:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
final_vessel_id = vessel_id
|
|
||||||
final_to_vessel_id = extract_vessel_id(to_vessel) or extract_vessel_id(product_vessel)
|
|
||||||
final_waste_vessel_id = extract_vessel_id(waste_phase_to_vessel) or extract_vessel_id(waste_vessel)
|
|
||||||
|
|
||||||
# 统一体积参数
|
|
||||||
final_volume = parse_volume_input(volume or solvent_volume)
|
|
||||||
|
|
||||||
# 🔧 修复:确保repeats至少为1
|
|
||||||
if repeats <= 0:
|
|
||||||
repeats = 1
|
|
||||||
debug_print(f"⚠️ 重复次数参数 <= 0,自动设置为 1")
|
|
||||||
|
|
||||||
debug_print(f"🔧 标准化后的参数:")
|
|
||||||
debug_print(f" 🥼 分离容器: '{final_vessel_id}'")
|
|
||||||
debug_print(f" 🎯 产物容器: '{final_to_vessel_id}'")
|
|
||||||
debug_print(f" 🗑️ 废液容器: '{final_waste_vessel_id}'")
|
|
||||||
debug_print(f" 📏 溶剂体积: {final_volume}mL")
|
|
||||||
debug_print(f" 🔄 重复次数: {repeats}")
|
|
||||||
|
|
||||||
action_sequence.append(create_action_log(f"分离容器: {final_vessel_id}", "🧪"))
|
|
||||||
action_sequence.append(create_action_log(f"溶剂体积: {final_volume}mL", "📏"))
|
|
||||||
action_sequence.append(create_action_log(f"重复次数: {repeats}", "🔄"))
|
|
||||||
|
|
||||||
# 验证必需参数
|
|
||||||
if not purpose:
|
|
||||||
purpose = "separate"
|
|
||||||
if not product_phase:
|
|
||||||
product_phase = "top"
|
|
||||||
if purpose not in ["wash", "extract", "separate"]:
|
|
||||||
debug_print(f"⚠️ 未知的分离目的 '{purpose}',使用默认值 'separate'")
|
|
||||||
purpose = "separate"
|
|
||||||
action_sequence.append(create_action_log(f"未知目的,使用: {purpose}", "⚠️"))
|
|
||||||
if product_phase not in ["top", "bottom"]:
|
|
||||||
debug_print(f"⚠️ 未知的产物相 '{product_phase}',使用默认值 'top'")
|
|
||||||
product_phase = "top"
|
|
||||||
action_sequence.append(create_action_log(f"未知相别,使用: {product_phase}", "⚠️"))
|
|
||||||
|
|
||||||
debug_print("✅ 参数验证通过")
|
|
||||||
action_sequence.append(create_action_log("参数验证通过", "✅"))
|
|
||||||
|
|
||||||
# === 查找设备 ===
|
|
||||||
debug_print("🔍 步骤2: 查找设备...")
|
|
||||||
action_sequence.append(create_action_log("正在查找相关设备...", "🔍"))
|
|
||||||
|
|
||||||
# 查找分离器设备
|
|
||||||
separator_device = find_separator_device(G, final_vessel_id) # 🔧 使用 final_vessel_id
|
|
||||||
if separator_device:
|
|
||||||
action_sequence.append(create_action_log(f"找到分离器设备: {separator_device}", "🧪"))
|
|
||||||
else:
|
|
||||||
debug_print("⚠️ 未找到分离器设备,可能无法执行分离")
|
|
||||||
action_sequence.append(create_action_log("未找到分离器设备", "⚠️"))
|
|
||||||
|
|
||||||
# 查找搅拌器
|
|
||||||
stirrer_device = find_connected_stirrer(G, final_vessel_id) # 🔧 使用 final_vessel_id
|
|
||||||
if stirrer_device:
|
|
||||||
action_sequence.append(create_action_log(f"找到搅拌器: {stirrer_device}", "🌪️"))
|
|
||||||
else:
|
|
||||||
action_sequence.append(create_action_log("未找到搅拌器", "⚠️"))
|
|
||||||
|
|
||||||
# 查找溶剂容器(如果需要)
|
|
||||||
solvent_vessel = ""
|
|
||||||
if solvent and solvent.strip():
|
|
||||||
solvent_vessel = find_solvent_vessel(G, solvent)
|
|
||||||
if solvent_vessel:
|
|
||||||
action_sequence.append(create_action_log(f"找到溶剂容器: {solvent_vessel}", "💧"))
|
|
||||||
else:
|
|
||||||
action_sequence.append(create_action_log(f"未找到溶剂容器: {solvent}", "⚠️"))
|
|
||||||
|
|
||||||
debug_print(f"📊 设备配置:")
|
|
||||||
debug_print(f" 🧪 分离器设备: '{separator_device}'")
|
|
||||||
debug_print(f" 🌪️ 搅拌器设备: '{stirrer_device}'")
|
|
||||||
debug_print(f" 💧 溶剂容器: '{solvent_vessel}'")
|
|
||||||
|
|
||||||
# === 执行分离流程 ===
|
|
||||||
debug_print("🔍 步骤3: 执行分离流程...")
|
|
||||||
action_sequence.append(create_action_log("开始分离工作流程", "🎯"))
|
|
||||||
|
|
||||||
# 🔧 新增:体积变化跟踪变量
|
|
||||||
current_volume = original_liquid_volume
|
|
||||||
|
|
||||||
try:
|
|
||||||
for repeat_idx in range(repeats):
|
|
||||||
cycle_num = repeat_idx + 1
|
|
||||||
debug_print(f"🔄 第{cycle_num}轮: 开始分离循环 {cycle_num}/{repeats}")
|
|
||||||
action_sequence.append(create_action_log(f"分离循环 {cycle_num}/{repeats} 开始", "🔄"))
|
|
||||||
|
|
||||||
# 步骤3.1: 添加溶剂(如果需要)
|
|
||||||
if solvent_vessel and final_volume > 0:
|
|
||||||
debug_print(f"🔄 第{cycle_num}轮 步骤1: 添加溶剂 {solvent} ({final_volume}mL)")
|
|
||||||
action_sequence.append(create_action_log(f"向分离容器添加 {final_volume}mL {solvent}", "💧"))
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 使用pump protocol添加溶剂
|
|
||||||
pump_actions = generate_pump_protocol_with_rinsing(
|
|
||||||
G=G,
|
|
||||||
from_vessel=solvent_vessel,
|
|
||||||
to_vessel=final_vessel_id, # 🔧 使用 final_vessel_id
|
|
||||||
volume=final_volume,
|
|
||||||
amount="",
|
|
||||||
time=0.0,
|
|
||||||
viscous=False,
|
|
||||||
rinsing_solvent="",
|
|
||||||
rinsing_volume=0.0,
|
|
||||||
rinsing_repeats=0,
|
|
||||||
solid=False,
|
|
||||||
flowrate=2.5,
|
|
||||||
transfer_flowrate=0.5,
|
|
||||||
rate_spec="",
|
|
||||||
event="",
|
|
||||||
through="",
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
action_sequence.extend(pump_actions)
|
|
||||||
debug_print(f"✅ 溶剂添加完成,添加了 {len(pump_actions)} 个动作")
|
|
||||||
action_sequence.append(create_action_log(f"溶剂转移完成 ({len(pump_actions)} 个操作)", "✅"))
|
|
||||||
|
|
||||||
# 🔧 新增:更新体积 - 添加溶剂后
|
|
||||||
current_volume += final_volume
|
|
||||||
update_vessel_volume(vessel, G, current_volume, f"添加{final_volume}mL {solvent}后")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
debug_print(f"❌ 溶剂添加失败: {str(e)}")
|
|
||||||
action_sequence.append(create_action_log(f"溶剂添加失败: {str(e)}", "❌"))
|
|
||||||
else:
|
|
||||||
debug_print(f"🔄 第{cycle_num}轮 步骤1: 无需添加溶剂")
|
|
||||||
action_sequence.append(create_action_log("无需添加溶剂", "⏭️"))
|
|
||||||
|
|
||||||
# 步骤3.2: 启动搅拌(如果有搅拌器)
|
|
||||||
if stirrer_device and stir_time > 0:
|
|
||||||
debug_print(f"🔄 第{cycle_num}轮 步骤2: 开始搅拌 ({stir_speed}rpm,持续 {stir_time}s)")
|
|
||||||
action_sequence.append(create_action_log(f"开始搅拌: {stir_speed}rpm,持续 {stir_time}s", "🌪️"))
|
|
||||||
|
|
||||||
action_sequence.append({
|
|
||||||
"device_id": stirrer_device,
|
|
||||||
"action_name": "start_stir",
|
|
||||||
"action_kwargs": {
|
|
||||||
"vessel": final_vessel_id, # 🔧 使用 final_vessel_id
|
|
||||||
"stir_speed": stir_speed,
|
|
||||||
"purpose": f"分离混合 - {purpose}"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# 搅拌等待
|
|
||||||
stir_minutes = stir_time / 60
|
|
||||||
action_sequence.append(create_action_log(f"搅拌中,持续 {stir_minutes:.1f} 分钟", "⏱️"))
|
|
||||||
action_sequence.append({
|
|
||||||
"action_name": "wait",
|
|
||||||
"action_kwargs": {"time": stir_time}
|
|
||||||
})
|
|
||||||
|
|
||||||
# 停止搅拌
|
|
||||||
action_sequence.append(create_action_log("停止搅拌器", "🛑"))
|
|
||||||
action_sequence.append({
|
|
||||||
"device_id": stirrer_device,
|
|
||||||
"action_name": "stop_stir",
|
|
||||||
"action_kwargs": {"vessel": final_vessel_id} # 🔧 使用 final_vessel_id
|
|
||||||
})
|
|
||||||
|
|
||||||
else:
|
|
||||||
debug_print(f"🔄 第{cycle_num}轮 步骤2: 无需搅拌")
|
|
||||||
action_sequence.append(create_action_log("无需搅拌", "⏭️"))
|
|
||||||
|
|
||||||
# 步骤3.3: 静置分层
|
|
||||||
if settling_time > 0:
|
|
||||||
debug_print(f"🔄 第{cycle_num}轮 步骤3: 静置分层 ({settling_time}s)")
|
|
||||||
settling_minutes = settling_time / 60
|
|
||||||
action_sequence.append(create_action_log(f"静置分层 ({settling_minutes:.1f} 分钟)", "⚖️"))
|
|
||||||
action_sequence.append({
|
|
||||||
"action_name": "wait",
|
|
||||||
"action_kwargs": {"time": settling_time}
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
debug_print(f"🔄 第{cycle_num}轮 步骤3: 未指定静置时间")
|
|
||||||
action_sequence.append(create_action_log("未指定静置时间", "⏭️"))
|
|
||||||
|
|
||||||
# 步骤3.4: 执行分离操作
|
|
||||||
if separator_device:
|
|
||||||
debug_print(f"🔄 第{cycle_num}轮 步骤4: 执行分离操作")
|
|
||||||
action_sequence.append(create_action_log(f"执行分离: 收集{product_phase}相", "🧪"))
|
|
||||||
|
|
||||||
# 调用分离器设备的separate方法
|
|
||||||
separate_action = {
|
|
||||||
"device_id": separator_device,
|
|
||||||
"action_name": "separate",
|
|
||||||
"action_kwargs": {
|
|
||||||
"purpose": purpose,
|
|
||||||
"product_phase": product_phase,
|
|
||||||
"from_vessel": extract_vessel_id(from_vessel) or final_vessel_id, # 🔧 使用vessel_id
|
|
||||||
"separation_vessel": final_vessel_id, # 🔧 使用 final_vessel_id
|
|
||||||
"to_vessel": final_to_vessel_id or final_vessel_id, # 🔧 使用vessel_id
|
|
||||||
"waste_phase_to_vessel": final_waste_vessel_id or final_vessel_id, # 🔧 使用vessel_id
|
|
||||||
"solvent": solvent,
|
|
||||||
"solvent_volume": final_volume,
|
|
||||||
"through": through,
|
|
||||||
"repeats": 1, # 每次调用只做一次分离
|
|
||||||
"stir_time": 0, # 已经在上面完成
|
|
||||||
"stir_speed": stir_speed,
|
|
||||||
"settling_time": 0 # 已经在上面完成
|
|
||||||
}
|
|
||||||
}
|
|
||||||
action_sequence.append(separate_action)
|
|
||||||
debug_print(f"✅ 分离操作已添加")
|
|
||||||
action_sequence.append(create_action_log("分离操作完成", "✅"))
|
|
||||||
|
|
||||||
# 🔧 新增:分离后体积估算(分离通常不改变总体积,但会重新分配)
|
|
||||||
# 假设分离后保持体积(实际情况可能有少量损失)
|
|
||||||
separated_volume = current_volume * 0.95 # 假设5%损失
|
|
||||||
update_vessel_volume(vessel, G, separated_volume, f"分离操作后(第{cycle_num}轮)")
|
|
||||||
current_volume = separated_volume
|
|
||||||
|
|
||||||
# 收集结果
|
|
||||||
if final_to_vessel_id:
|
|
||||||
action_sequence.append(create_action_log(f"产物 ({product_phase}相) 收集到: {final_to_vessel_id}", "📦"))
|
|
||||||
if final_waste_vessel_id:
|
|
||||||
action_sequence.append(create_action_log(f"废相收集到: {final_waste_vessel_id}", "🗑️"))
|
|
||||||
|
|
||||||
else:
|
|
||||||
debug_print(f"🔄 第{cycle_num}轮 步骤4: 无分离器设备,跳过分离")
|
|
||||||
action_sequence.append(create_action_log("无分离器设备可用", "❌"))
|
|
||||||
# 添加等待时间模拟分离
|
|
||||||
action_sequence.append({
|
|
||||||
"action_name": "wait",
|
|
||||||
"action_kwargs": {"time": 10.0}
|
|
||||||
})
|
|
||||||
|
|
||||||
# 循环间等待(除了最后一次)
|
|
||||||
if repeat_idx < repeats - 1:
|
|
||||||
debug_print(f"🔄 第{cycle_num}轮: 等待下一次循环...")
|
|
||||||
action_sequence.append(create_action_log("等待下一次循环...", "⏳"))
|
|
||||||
action_sequence.append({
|
|
||||||
"action_name": "wait",
|
|
||||||
"action_kwargs": {"time": 5}
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
action_sequence.append(create_action_log(f"分离循环 {cycle_num}/{repeats} 完成", "🌟"))
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
debug_print(f"❌ 分离工作流程执行失败: {str(e)}")
|
|
||||||
action_sequence.append(create_action_log(f"分离工作流程失败: {str(e)}", "❌"))
|
|
||||||
# 添加错误日志
|
|
||||||
action_sequence.append({
|
|
||||||
"device_id": "system",
|
|
||||||
"action_name": "log_message",
|
|
||||||
"action_kwargs": {
|
|
||||||
"message": f"分离操作失败: {str(e)}"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# 🔧 新增:分离完成后的最终状态报告
|
|
||||||
final_liquid_volume = get_vessel_liquid_volume(vessel)
|
|
||||||
|
|
||||||
# === 最终结果 ===
|
|
||||||
total_time = (stir_time + settling_time + 15) * repeats # 估算总时间
|
|
||||||
|
|
||||||
debug_print("🌀" * 20)
|
|
||||||
debug_print(f"🎉 分离协议生成完成")
|
|
||||||
debug_print(f"📊 协议统计:")
|
|
||||||
debug_print(f" 📋 总动作数: {len(action_sequence)}")
|
|
||||||
debug_print(f" ⏱️ 预计总时间: {total_time:.0f}s ({total_time/60:.1f} 分钟)")
|
|
||||||
debug_print(f" 🥼 分离容器: {final_vessel_id}")
|
|
||||||
debug_print(f" 🎯 分离目的: {purpose}")
|
|
||||||
debug_print(f" 📊 产物相: {product_phase}")
|
|
||||||
debug_print(f" 🔄 重复次数: {repeats}")
|
|
||||||
debug_print(f"💧 体积变化统计:")
|
|
||||||
debug_print(f" - 分离前体积: {original_liquid_volume:.2f}mL")
|
|
||||||
debug_print(f" - 分离后体积: {final_liquid_volume:.2f}mL")
|
|
||||||
if solvent:
|
|
||||||
debug_print(f" 💧 溶剂: {solvent} ({final_volume}mL × {repeats}轮 = {final_volume * repeats:.2f}mL)")
|
|
||||||
if final_to_vessel_id:
|
|
||||||
debug_print(f" 🎯 产物容器: {final_to_vessel_id}")
|
|
||||||
if final_waste_vessel_id:
|
|
||||||
debug_print(f" 🗑️ 废液容器: {final_waste_vessel_id}")
|
|
||||||
debug_print("🌀" * 20)
|
|
||||||
|
|
||||||
# 添加完成日志
|
|
||||||
summary_msg = f"分离协议完成: {final_vessel_id} ({purpose},{repeats} 次循环)"
|
|
||||||
if solvent:
|
|
||||||
summary_msg += f",使用 {final_volume * repeats:.2f}mL {solvent}"
|
|
||||||
action_sequence.append(create_action_log(summary_msg, "🎉"))
|
|
||||||
|
|
||||||
return action_sequence
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,81 +3,14 @@ import networkx as nx
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
from .utils.unit_parser import parse_time_input
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def debug_print(message):
|
def debug_print(message):
|
||||||
"""调试输出"""
|
"""调试输出"""
|
||||||
print(f"🌪️ [STIR] {message}", flush=True)
|
|
||||||
logger.info(f"[STIR] {message}")
|
logger.info(f"[STIR] {message}")
|
||||||
|
|
||||||
def parse_time_input(time_input: Union[str, float, int], default_unit: str = "s") -> float:
|
|
||||||
"""
|
|
||||||
统一的时间解析函数(精简版)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
time_input: 时间输入(如 "30 min", "1 h", "300", "?", 60.0)
|
|
||||||
default_unit: 默认单位(默认为秒)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: 时间(秒)
|
|
||||||
"""
|
|
||||||
if not time_input:
|
|
||||||
return 100.0 # 默认100秒
|
|
||||||
|
|
||||||
# 🔢 处理数值输入
|
|
||||||
if isinstance(time_input, (int, float)):
|
|
||||||
result = float(time_input)
|
|
||||||
debug_print(f"⏰ 数值时间: {time_input} → {result}s")
|
|
||||||
return result
|
|
||||||
|
|
||||||
# 📝 处理字符串输入
|
|
||||||
time_str = str(time_input).lower().strip()
|
|
||||||
debug_print(f"🔍 解析时间: '{time_str}'")
|
|
||||||
|
|
||||||
# ❓ 特殊值处理
|
|
||||||
special_times = {
|
|
||||||
'?': 300.0, 'unknown': 300.0, 'tbd': 300.0,
|
|
||||||
'briefly': 30.0, 'quickly': 60.0, 'slowly': 600.0,
|
|
||||||
'several minutes': 300.0, 'few minutes': 180.0, 'overnight': 3600.0
|
|
||||||
}
|
|
||||||
|
|
||||||
if time_str in special_times:
|
|
||||||
result = special_times[time_str]
|
|
||||||
debug_print(f"🎯 特殊时间: '{time_str}' → {result}s ({result/60:.1f}分钟)")
|
|
||||||
return result
|
|
||||||
|
|
||||||
# 🔢 纯数字处理
|
|
||||||
try:
|
|
||||||
result = float(time_str)
|
|
||||||
debug_print(f"⏰ 纯数字: {time_str} → {result}s")
|
|
||||||
return result
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 📐 正则表达式解析
|
|
||||||
pattern = r'(\d+\.?\d*)\s*([a-z]*)'
|
|
||||||
match = re.match(pattern, time_str)
|
|
||||||
|
|
||||||
if not match:
|
|
||||||
debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值: 100s")
|
|
||||||
return 100.0
|
|
||||||
|
|
||||||
value = float(match.group(1))
|
|
||||||
unit = match.group(2) or default_unit
|
|
||||||
|
|
||||||
# 📏 单位转换
|
|
||||||
unit_multipliers = {
|
|
||||||
's': 1.0, 'sec': 1.0, 'second': 1.0, 'seconds': 1.0,
|
|
||||||
'm': 60.0, 'min': 60.0, 'mins': 60.0, 'minute': 60.0, 'minutes': 60.0,
|
|
||||||
'h': 3600.0, 'hr': 3600.0, 'hrs': 3600.0, 'hour': 3600.0, 'hours': 3600.0,
|
|
||||||
'd': 86400.0, 'day': 86400.0, 'days': 86400.0
|
|
||||||
}
|
|
||||||
|
|
||||||
multiplier = unit_multipliers.get(unit, 1.0)
|
|
||||||
result = value * multiplier
|
|
||||||
|
|
||||||
debug_print(f"✅ 时间解析: '{time_str}' → {value} {unit} → {result}s ({result/60:.1f}分钟)")
|
|
||||||
return result
|
|
||||||
|
|
||||||
def find_connected_stirrer(G: nx.DiGraph, vessel: str = None) -> str:
|
def find_connected_stirrer(G: nx.DiGraph, vessel: str = None) -> str:
|
||||||
"""查找与指定容器相连的搅拌设备"""
|
"""查找与指定容器相连的搅拌设备"""
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
from typing import List, Dict, Any
|
|
||||||
import networkx as nx
|
|
||||||
|
|
||||||
def generate_transfer_protocol(
|
|
||||||
G: nx.DiGraph,
|
|
||||||
from_vessel: str,
|
|
||||||
to_vessel: str,
|
|
||||||
volume: float,
|
|
||||||
amount: str = "",
|
|
||||||
time: float = 0,
|
|
||||||
viscous: bool = False,
|
|
||||||
rinsing_solvent: str = "",
|
|
||||||
rinsing_volume: float = 0.0,
|
|
||||||
rinsing_repeats: int = 0,
|
|
||||||
solid: bool = False
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
生成液体转移操作的协议序列
|
|
||||||
|
|
||||||
Args:
|
|
||||||
G: 有向图,节点为设备和容器
|
|
||||||
from_vessel: 源容器
|
|
||||||
to_vessel: 目标容器
|
|
||||||
volume: 转移体积 (mL)
|
|
||||||
amount: 数量描述 (可选)
|
|
||||||
time: 转移时间 (秒,可选)
|
|
||||||
viscous: 是否为粘性液体
|
|
||||||
rinsing_solvent: 冲洗溶剂 (可选)
|
|
||||||
rinsing_volume: 冲洗体积 (mL,可选)
|
|
||||||
rinsing_repeats: 冲洗重复次数
|
|
||||||
solid: 是否涉及固体
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List[Dict[str, Any]]: 转移操作的动作序列
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: 当找不到合适的转移设备时抛出异常
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
transfer_protocol = generate_transfer_protocol(G, "flask_1", "reactor", 10.0)
|
|
||||||
"""
|
|
||||||
action_sequence = []
|
|
||||||
|
|
||||||
# 查找虚拟转移泵设备用于液体转移 - 修复:应该查找 virtual_transfer_pump
|
|
||||||
pump_nodes = [node for node in G.nodes()
|
|
||||||
if G.nodes[node].get('class') == 'virtual_transfer_pump']
|
|
||||||
|
|
||||||
if not pump_nodes:
|
|
||||||
raise ValueError("没有找到可用的转移泵设备进行液体转移")
|
|
||||||
|
|
||||||
# 使用第一个可用的泵
|
|
||||||
pump_id = pump_nodes[0]
|
|
||||||
|
|
||||||
# 验证容器是否存在
|
|
||||||
if from_vessel not in G.nodes():
|
|
||||||
raise ValueError(f"源容器 {from_vessel} 不存在于图中")
|
|
||||||
|
|
||||||
if to_vessel not in G.nodes():
|
|
||||||
raise ValueError(f"目标容器 {to_vessel} 不存在于图中")
|
|
||||||
|
|
||||||
# 执行液体转移操作 - 参数完全匹配Transfer.action
|
|
||||||
action_sequence.append({
|
|
||||||
"device_id": pump_id,
|
|
||||||
"action_name": "transfer",
|
|
||||||
"action_kwargs": {
|
|
||||||
"from_vessel": from_vessel,
|
|
||||||
"to_vessel": to_vessel,
|
|
||||||
"volume": volume,
|
|
||||||
"amount": amount,
|
|
||||||
"time": time,
|
|
||||||
"viscous": viscous,
|
|
||||||
"rinsing_solvent": rinsing_solvent,
|
|
||||||
"rinsing_volume": rinsing_volume,
|
|
||||||
"rinsing_repeats": rinsing_repeats,
|
|
||||||
"solid": solid
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return action_sequence
|
|
||||||
36
unilabos/compile/utils/logger_util.py
Normal file
36
unilabos/compile/utils/logger_util.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# 🆕 创建进度日志动作
|
||||||
|
import logging
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def debug_print(message, prefix="[UNIT_PARSER]"):
|
||||||
|
"""调试输出"""
|
||||||
|
logger.info(f"{prefix} {message}")
|
||||||
|
|
||||||
|
|
||||||
|
def action_log(message: str, emoji: str = "📝", prefix="[HIGH-LEVEL OPERATION]") -> Dict[str, Any]:
|
||||||
|
"""创建一个动作日志 - 支持中文和emoji"""
|
||||||
|
try:
|
||||||
|
full_message = f"{prefix} {emoji} {message}"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"action_name": "wait",
|
||||||
|
"action_kwargs": {
|
||||||
|
"time": 0.1,
|
||||||
|
"log_message": full_message,
|
||||||
|
"progress_message": full_message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
# 如果emoji有问题,使用纯文本
|
||||||
|
safe_message = f"{prefix} {message}"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"action_name": "wait",
|
||||||
|
"action_kwargs": {
|
||||||
|
"time": 0.1,
|
||||||
|
"log_message": safe_message,
|
||||||
|
"progress_message": safe_message
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,108 +4,12 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import logging
|
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
from .logger_util import debug_print
|
||||||
|
|
||||||
def debug_print(message, prefix="[UNIT_PARSER]"):
|
|
||||||
"""调试输出"""
|
|
||||||
print(f"{prefix} {message}", flush=True)
|
|
||||||
logger.info(f"{prefix} {message}")
|
|
||||||
|
|
||||||
def parse_time_with_units(time_input: Union[str, float, int], default_unit: str = "s") -> float:
|
def parse_volume_input(volume_input: Union[str, float, int], default_unit: str = "mL") -> float:
|
||||||
"""
|
|
||||||
解析带单位的时间输入
|
|
||||||
|
|
||||||
Args:
|
|
||||||
time_input: 时间输入(如 "30 min", "1 h", "300", "?", 60.0)
|
|
||||||
default_unit: 默认单位(默认为秒)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: 时间(秒)
|
|
||||||
"""
|
|
||||||
if not time_input:
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
# 处理数值输入
|
|
||||||
if isinstance(time_input, (int, float)):
|
|
||||||
result = float(time_input)
|
|
||||||
debug_print(f"数值时间输入: {time_input} → {result}s(默认单位)")
|
|
||||||
return result
|
|
||||||
|
|
||||||
# 处理字符串输入
|
|
||||||
time_str = str(time_input).lower().strip()
|
|
||||||
debug_print(f"解析时间字符串: '{time_str}'")
|
|
||||||
|
|
||||||
# 处理特殊值
|
|
||||||
if time_str in ['?', 'unknown', 'tbd', 'to be determined']:
|
|
||||||
default_time = 300.0 # 5分钟默认值
|
|
||||||
debug_print(f"检测到未知时间,使用默认值: {default_time}s")
|
|
||||||
return default_time
|
|
||||||
|
|
||||||
# 如果是纯数字,使用默认单位
|
|
||||||
try:
|
|
||||||
value = float(time_str)
|
|
||||||
if default_unit == "s":
|
|
||||||
result = value
|
|
||||||
elif default_unit in ["min", "minute"]:
|
|
||||||
result = value * 60.0
|
|
||||||
elif default_unit in ["h", "hour"]:
|
|
||||||
result = value * 3600.0
|
|
||||||
else:
|
|
||||||
result = value # 默认秒
|
|
||||||
debug_print(f"纯数字输入: {time_str} → {result}s(单位: {default_unit})")
|
|
||||||
return result
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 使用正则表达式匹配数字和单位
|
|
||||||
pattern = r'(\d+\.?\d*)\s*([a-z]*)'
|
|
||||||
match = re.match(pattern, time_str)
|
|
||||||
|
|
||||||
if not match:
|
|
||||||
debug_print(f"⚠️ 无法解析时间: '{time_str}',使用默认值: 60s")
|
|
||||||
return 60.0
|
|
||||||
|
|
||||||
value = float(match.group(1))
|
|
||||||
unit = match.group(2) or default_unit
|
|
||||||
|
|
||||||
# 单位转换映射
|
|
||||||
unit_multipliers = {
|
|
||||||
# 秒
|
|
||||||
's': 1.0,
|
|
||||||
'sec': 1.0,
|
|
||||||
'second': 1.0,
|
|
||||||
'seconds': 1.0,
|
|
||||||
|
|
||||||
# 分钟
|
|
||||||
'm': 60.0,
|
|
||||||
'min': 60.0,
|
|
||||||
'mins': 60.0,
|
|
||||||
'minute': 60.0,
|
|
||||||
'minutes': 60.0,
|
|
||||||
|
|
||||||
# 小时
|
|
||||||
'h': 3600.0,
|
|
||||||
'hr': 3600.0,
|
|
||||||
'hrs': 3600.0,
|
|
||||||
'hour': 3600.0,
|
|
||||||
'hours': 3600.0,
|
|
||||||
|
|
||||||
# 天
|
|
||||||
'd': 86400.0,
|
|
||||||
'day': 86400.0,
|
|
||||||
'days': 86400.0,
|
|
||||||
}
|
|
||||||
|
|
||||||
multiplier = unit_multipliers.get(unit, 1.0)
|
|
||||||
result = value * multiplier
|
|
||||||
|
|
||||||
debug_print(f"时间解析: '{time_str}' → {value} {unit} → {result}s")
|
|
||||||
return result
|
|
||||||
|
|
||||||
def parse_volume_with_units(volume_input: Union[str, float, int], default_unit: str = "mL") -> float:
|
|
||||||
"""
|
"""
|
||||||
解析带单位的体积输入
|
解析带单位的体积输入
|
||||||
|
|
||||||
@@ -175,6 +79,111 @@ def parse_volume_with_units(volume_input: Union[str, float, int], default_unit:
|
|||||||
debug_print(f"体积解析: '{volume_str}' → {value} {unit} → {volume}mL")
|
debug_print(f"体积解析: '{volume_str}' → {value} {unit} → {volume}mL")
|
||||||
return volume
|
return volume
|
||||||
|
|
||||||
|
|
||||||
|
def parse_mass_input(mass_input: Union[str, float]) -> float:
|
||||||
|
"""
|
||||||
|
解析质量输入,支持带单位的字符串
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mass_input: 质量输入(如 "19.3 g", "4.5 g", 2.5)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: 质量(克)
|
||||||
|
"""
|
||||||
|
if isinstance(mass_input, (int, float)):
|
||||||
|
debug_print(f"⚖️ 质量输入为数值: {mass_input}g")
|
||||||
|
return float(mass_input)
|
||||||
|
|
||||||
|
if not mass_input or not str(mass_input).strip():
|
||||||
|
debug_print(f"⚠️ 质量输入为空,返回0.0g")
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
mass_str = str(mass_input).lower().strip()
|
||||||
|
debug_print(f"🔍 解析质量输入: '{mass_str}'")
|
||||||
|
|
||||||
|
# 移除空格并提取数字和单位
|
||||||
|
mass_clean = re.sub(r'\s+', '', mass_str)
|
||||||
|
|
||||||
|
# 匹配数字和单位的正则表达式
|
||||||
|
match = re.match(r'([0-9]*\.?[0-9]+)\s*(g|mg|kg|gram|milligram|kilogram)?', mass_clean)
|
||||||
|
|
||||||
|
if not match:
|
||||||
|
debug_print(f"❌ 无法解析质量: '{mass_str}',返回0.0g")
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
value = float(match.group(1))
|
||||||
|
unit = match.group(2) or 'g' # 默认单位为克
|
||||||
|
|
||||||
|
# 转换为克
|
||||||
|
if unit in ['mg', 'milligram']:
|
||||||
|
mass = value / 1000.0 # mg -> g
|
||||||
|
debug_print(f"🔄 质量转换: {value}mg → {mass}g")
|
||||||
|
elif unit in ['kg', 'kilogram']:
|
||||||
|
mass = value * 1000.0 # kg -> g
|
||||||
|
debug_print(f"🔄 质量转换: {value}kg → {mass}g")
|
||||||
|
else: # g, gram 或默认
|
||||||
|
mass = value # 已经是g
|
||||||
|
debug_print(f"✅ 质量已为g: {mass}g")
|
||||||
|
|
||||||
|
return mass
|
||||||
|
|
||||||
|
|
||||||
|
def parse_time_input(time_input: Union[str, float]) -> float:
|
||||||
|
"""
|
||||||
|
解析时间输入,支持带单位的字符串
|
||||||
|
|
||||||
|
Args:
|
||||||
|
time_input: 时间输入(如 "1 h", "20 min", "30 s", 60.0)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: 时间(秒)
|
||||||
|
"""
|
||||||
|
if isinstance(time_input, (int, float)):
|
||||||
|
debug_print(f"⏱️ 时间输入为数值: {time_input}秒")
|
||||||
|
return float(time_input)
|
||||||
|
|
||||||
|
if not time_input or not str(time_input).strip():
|
||||||
|
debug_print(f"⚠️ 时间输入为空,返回0秒")
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
time_str = str(time_input).lower().strip()
|
||||||
|
debug_print(f"🔍 解析时间输入: '{time_str}'")
|
||||||
|
|
||||||
|
# 处理未知时间
|
||||||
|
if time_str in ['?', 'unknown', 'tbd']:
|
||||||
|
default_time = 60.0 # 默认1分钟
|
||||||
|
debug_print(f"❓ 检测到未知时间,使用默认值: {default_time}s (1分钟) ⏰")
|
||||||
|
return default_time
|
||||||
|
|
||||||
|
# 移除空格并提取数字和单位
|
||||||
|
time_clean = re.sub(r'\s+', '', time_str)
|
||||||
|
|
||||||
|
# 匹配数字和单位的正则表达式
|
||||||
|
match = re.match(r'([0-9]*\.?[0-9]+)\s*(s|sec|second|min|minute|h|hr|hour|d|day)?', time_clean)
|
||||||
|
|
||||||
|
if not match:
|
||||||
|
debug_print(f"❌ 无法解析时间: '{time_str}',返回0s")
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
value = float(match.group(1))
|
||||||
|
unit = match.group(2) or 's' # 默认单位为秒
|
||||||
|
|
||||||
|
# 转换为秒
|
||||||
|
if unit in ['m', 'min', 'minute', 'mins', 'minutes']:
|
||||||
|
time_sec = value * 60.0 # min -> s
|
||||||
|
debug_print(f"🔄 时间转换: {value}分钟 → {time_sec}秒")
|
||||||
|
elif unit in ['h', 'hr', 'hour', 'hrs', 'hours']:
|
||||||
|
time_sec = value * 3600.0 # h -> s
|
||||||
|
debug_print(f"🔄 时间转换: {value}小时 → {time_sec}秒")
|
||||||
|
elif unit in ['d', 'day', 'days']:
|
||||||
|
time_sec = value * 86400.0 # d -> s
|
||||||
|
debug_print(f"🔄 时间转换: {value}天 → {time_sec}秒")
|
||||||
|
else: # s, sec, second 或默认
|
||||||
|
time_sec = value # 已经是s
|
||||||
|
debug_print(f"✅ 时间已为秒: {time_sec}秒")
|
||||||
|
|
||||||
|
return time_sec
|
||||||
|
|
||||||
# 测试函数
|
# 测试函数
|
||||||
def test_unit_parser():
|
def test_unit_parser():
|
||||||
"""测试单位解析功能"""
|
"""测试单位解析功能"""
|
||||||
@@ -187,7 +196,7 @@ def test_unit_parser():
|
|||||||
|
|
||||||
print("\n时间解析测试:")
|
print("\n时间解析测试:")
|
||||||
for time_input in time_tests:
|
for time_input in time_tests:
|
||||||
result = parse_time_with_units(time_input)
|
result = parse_time_input(time_input)
|
||||||
print(f" {time_input} → {result}s ({result/60:.1f}min)")
|
print(f" {time_input} → {result}s ({result/60:.1f}min)")
|
||||||
|
|
||||||
# 测试体积解析
|
# 测试体积解析
|
||||||
@@ -197,7 +206,7 @@ def test_unit_parser():
|
|||||||
|
|
||||||
print("\n体积解析测试:")
|
print("\n体积解析测试:")
|
||||||
for volume_input in volume_tests:
|
for volume_input in volume_tests:
|
||||||
result = parse_volume_with_units(volume_input)
|
result = parse_volume_input(volume_input)
|
||||||
print(f" {volume_input} → {result}mL")
|
print(f" {volume_input} → {result}mL")
|
||||||
|
|
||||||
print("\n✅ 测试完成")
|
print("\n✅ 测试完成")
|
||||||
|
|||||||
281
unilabos/compile/utils/vessel_parser.py
Normal file
281
unilabos/compile/utils/vessel_parser.py
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
import networkx as nx
|
||||||
|
|
||||||
|
from .logger_util import debug_print
|
||||||
|
|
||||||
|
|
||||||
|
def get_vessel(vessel):
|
||||||
|
"""
|
||||||
|
统一处理vessel参数,返回vessel_id和vessel_data。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vessel: 可以是一个字典或字符串,表示vessel的ID或数据。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: 包含vessel_id和vessel_data。
|
||||||
|
"""
|
||||||
|
if isinstance(vessel, dict):
|
||||||
|
if "id" not in vessel:
|
||||||
|
vessel_id = list(vessel.values())[0].get("id", "")
|
||||||
|
else:
|
||||||
|
vessel_id = vessel.get("id", "")
|
||||||
|
vessel_data = vessel.get("data", {})
|
||||||
|
else:
|
||||||
|
vessel_id = str(vessel)
|
||||||
|
vessel_data = {}
|
||||||
|
return vessel_id, vessel_data
|
||||||
|
|
||||||
|
|
||||||
|
def find_reagent_vessel(G: nx.DiGraph, reagent: str) -> str:
|
||||||
|
"""增强版试剂容器查找,支持固体和液体"""
|
||||||
|
debug_print(f"🔍 开始查找试剂 '{reagent}' 的容器...")
|
||||||
|
|
||||||
|
# 🔧 方法1:直接搜索 data.reagent_name 和 config.reagent
|
||||||
|
debug_print(f"📋 方法1: 搜索reagent字段...")
|
||||||
|
for node in G.nodes():
|
||||||
|
node_data = G.nodes[node].get('data', {})
|
||||||
|
node_type = G.nodes[node].get('type', '')
|
||||||
|
config_data = G.nodes[node].get('config', {})
|
||||||
|
|
||||||
|
# 只搜索容器类型的节点
|
||||||
|
if node_type == 'container':
|
||||||
|
reagent_name = node_data.get('reagent_name', '').lower()
|
||||||
|
config_reagent = config_data.get('reagent', '').lower()
|
||||||
|
|
||||||
|
# 精确匹配
|
||||||
|
if reagent_name == reagent.lower() or config_reagent == reagent.lower():
|
||||||
|
debug_print(f"✅ 通过reagent字段精确匹配到容器: {node} 🎯")
|
||||||
|
return node
|
||||||
|
|
||||||
|
# 模糊匹配
|
||||||
|
if (reagent.lower() in reagent_name and reagent_name) or \
|
||||||
|
(reagent.lower() in config_reagent and config_reagent):
|
||||||
|
debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node} 🔍")
|
||||||
|
return node
|
||||||
|
|
||||||
|
# 🔧 方法2:常见的容器命名规则
|
||||||
|
debug_print(f"📋 方法2: 使用命名规则查找...")
|
||||||
|
reagent_clean = reagent.lower().replace(' ', '_').replace('-', '_')
|
||||||
|
possible_names = [
|
||||||
|
reagent_clean,
|
||||||
|
f"flask_{reagent_clean}",
|
||||||
|
f"bottle_{reagent_clean}",
|
||||||
|
f"vessel_{reagent_clean}",
|
||||||
|
f"{reagent_clean}_flask",
|
||||||
|
f"{reagent_clean}_bottle",
|
||||||
|
f"reagent_{reagent_clean}",
|
||||||
|
f"reagent_bottle_{reagent_clean}",
|
||||||
|
f"solid_reagent_bottle_{reagent_clean}",
|
||||||
|
f"reagent_bottle_1", # 通用试剂瓶
|
||||||
|
f"reagent_bottle_2",
|
||||||
|
f"reagent_bottle_3"
|
||||||
|
]
|
||||||
|
|
||||||
|
debug_print(f"🔍 尝试的容器名称: {possible_names[:5]}... (共{len(possible_names)}个)")
|
||||||
|
|
||||||
|
for name in possible_names:
|
||||||
|
if name in G.nodes():
|
||||||
|
node_type = G.nodes[name].get('type', '')
|
||||||
|
if node_type == 'container':
|
||||||
|
debug_print(f"✅ 通过命名规则找到容器: {name} 📝")
|
||||||
|
return name
|
||||||
|
|
||||||
|
# 🔧 方法3:节点名称模糊匹配
|
||||||
|
debug_print(f"📋 方法3: 节点名称模糊匹配...")
|
||||||
|
for node_id in G.nodes():
|
||||||
|
node_data = G.nodes[node_id]
|
||||||
|
if node_data.get('type') == 'container':
|
||||||
|
# 检查节点名称是否包含试剂名称
|
||||||
|
if reagent_clean in node_id.lower():
|
||||||
|
debug_print(f"✅ 通过节点名称模糊匹配到容器: {node_id} 🔍")
|
||||||
|
return node_id
|
||||||
|
|
||||||
|
# 检查液体类型匹配
|
||||||
|
vessel_data = node_data.get('data', {})
|
||||||
|
liquids = vessel_data.get('liquid', [])
|
||||||
|
for liquid in liquids:
|
||||||
|
if isinstance(liquid, dict):
|
||||||
|
liquid_type = liquid.get('liquid_type') or liquid.get('name', '')
|
||||||
|
if liquid_type.lower() == reagent.lower():
|
||||||
|
debug_print(f"✅ 通过液体类型匹配到容器: {node_id} 💧")
|
||||||
|
return node_id
|
||||||
|
|
||||||
|
# 🔧 方法4:使用第一个试剂瓶作为备选
|
||||||
|
debug_print(f"📋 方法4: 查找备选试剂瓶...")
|
||||||
|
for node_id in G.nodes():
|
||||||
|
node_data = G.nodes[node_id]
|
||||||
|
if (node_data.get('type') == 'container' and
|
||||||
|
('reagent' in node_id.lower() or 'bottle' in node_id.lower())):
|
||||||
|
debug_print(f"⚠️ 未找到专用容器,使用备选试剂瓶: {node_id} 🔄")
|
||||||
|
return node_id
|
||||||
|
|
||||||
|
debug_print(f"❌ 所有方法都失败了,无法找到容器!")
|
||||||
|
raise ValueError(f"找不到试剂 '{reagent}' 对应的容器")
|
||||||
|
|
||||||
|
|
||||||
|
def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
|
||||||
|
"""
|
||||||
|
查找溶剂容器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
G: 网络图
|
||||||
|
solvent: 溶剂名称
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 溶剂容器ID
|
||||||
|
"""
|
||||||
|
debug_print(f"🔍 正在查找溶剂 '{solvent}' 的容器... 🧪")
|
||||||
|
|
||||||
|
# 第四步:通过数据中的试剂信息匹配
|
||||||
|
debug_print(" 🧪 步骤1: 数据试剂信息匹配...")
|
||||||
|
for node_id in G.nodes():
|
||||||
|
debug_print(f"查找 id {node_id}, type={G.nodes[node_id].get('type')}, data={G.nodes[node_id].get('data', {})} 的容器...")
|
||||||
|
if G.nodes[node_id].get('type') == 'container':
|
||||||
|
vessel_data = G.nodes[node_id].get('data', {})
|
||||||
|
|
||||||
|
# 检查 data 中的 reagent_name 字段
|
||||||
|
reagent_name = vessel_data.get('reagent_name', '').lower()
|
||||||
|
if reagent_name and solvent.lower() == reagent_name:
|
||||||
|
debug_print(f" 🎉 通过data.reagent_name匹配找到容器: {node_id} (试剂: {reagent_name}) ✨")
|
||||||
|
return node_id
|
||||||
|
|
||||||
|
# 检查 data 中的液体信息
|
||||||
|
liquids = vessel_data.get('liquid', []) or vessel_data.get('liquids', [])
|
||||||
|
for liquid in liquids:
|
||||||
|
if isinstance(liquid, dict):
|
||||||
|
liquid_type = (liquid.get('liquid_type') or liquid.get('name', '')).lower()
|
||||||
|
|
||||||
|
if solvent.lower() == liquid_type or solvent.lower() in liquid_type:
|
||||||
|
debug_print(f" 🎉 通过液体类型匹配找到容器: {node_id} (液体类型: {liquid_type}) ✨")
|
||||||
|
return node_id
|
||||||
|
|
||||||
|
# 构建可能的容器名称
|
||||||
|
possible_names = [
|
||||||
|
f"flask_{solvent}",
|
||||||
|
f"bottle_{solvent}",
|
||||||
|
f"reagent_{solvent}",
|
||||||
|
f"reagent_bottle_{solvent}",
|
||||||
|
f"{solvent}_flask",
|
||||||
|
f"{solvent}_bottle",
|
||||||
|
f"{solvent}",
|
||||||
|
f"vessel_{solvent}",
|
||||||
|
]
|
||||||
|
|
||||||
|
debug_print(f"📋 候选容器名称: {possible_names[:3]}... (共{len(possible_names)}个) 📝")
|
||||||
|
|
||||||
|
# 第一步:通过容器名称匹配
|
||||||
|
debug_print(" 🎯 步骤2: 精确名称匹配...")
|
||||||
|
for vessel_name in possible_names:
|
||||||
|
if vessel_name in G.nodes():
|
||||||
|
debug_print(f" 🎉 通过名称匹配找到容器: {vessel_name} ✨")
|
||||||
|
return vessel_name
|
||||||
|
|
||||||
|
# 第二步:通过模糊匹配(节点ID和名称)
|
||||||
|
debug_print(" 🔍 步骤3: 模糊名称匹配...")
|
||||||
|
for node_id in G.nodes():
|
||||||
|
if G.nodes[node_id].get('type') == 'container':
|
||||||
|
node_name = G.nodes[node_id].get('name', '').lower()
|
||||||
|
|
||||||
|
if solvent.lower() in node_id.lower() or solvent.lower() in node_name:
|
||||||
|
debug_print(f" 🎉 通过模糊匹配找到容器: {node_id} (名称: {node_name}) ✨")
|
||||||
|
return node_id
|
||||||
|
|
||||||
|
# 第三步:通过配置中的试剂信息匹配
|
||||||
|
debug_print(" 🧪 步骤4: 配置试剂信息匹配...")
|
||||||
|
for node_id in G.nodes():
|
||||||
|
if G.nodes[node_id].get('type') == 'container':
|
||||||
|
# 检查 config 中的 reagent 字段
|
||||||
|
node_config = G.nodes[node_id].get('config', {})
|
||||||
|
config_reagent = node_config.get('reagent', '').lower()
|
||||||
|
|
||||||
|
if config_reagent and solvent.lower() == config_reagent:
|
||||||
|
debug_print(f" 🎉 通过config.reagent匹配找到容器: {node_id} (试剂: {config_reagent}) ✨")
|
||||||
|
return node_id
|
||||||
|
|
||||||
|
# 第五步:部分匹配(如果前面都没找到)
|
||||||
|
debug_print(" 🔍 步骤5: 部分匹配...")
|
||||||
|
for node_id in G.nodes():
|
||||||
|
if G.nodes[node_id].get('type') == 'container':
|
||||||
|
node_config = G.nodes[node_id].get('config', {})
|
||||||
|
node_data = G.nodes[node_id].get('data', {})
|
||||||
|
node_name = G.nodes[node_id].get('name', '').lower()
|
||||||
|
|
||||||
|
config_reagent = node_config.get('reagent', '').lower()
|
||||||
|
data_reagent = node_data.get('reagent_name', '').lower()
|
||||||
|
|
||||||
|
# 检查是否包含溶剂名称
|
||||||
|
if (solvent.lower() in config_reagent or
|
||||||
|
solvent.lower() in data_reagent or
|
||||||
|
solvent.lower() in node_name or
|
||||||
|
solvent.lower() in node_id.lower()):
|
||||||
|
debug_print(f" 🎉 通过部分匹配找到容器: {node_id} ✨")
|
||||||
|
debug_print(f" - 节点名称: {node_name}")
|
||||||
|
debug_print(f" - 配置试剂: {config_reagent}")
|
||||||
|
debug_print(f" - 数据试剂: {data_reagent}")
|
||||||
|
return node_id
|
||||||
|
|
||||||
|
# 调试信息:列出所有容器
|
||||||
|
debug_print(" 🔎 调试信息:列出所有容器...")
|
||||||
|
container_list = []
|
||||||
|
for node_id in G.nodes():
|
||||||
|
if G.nodes[node_id].get('type') == 'container':
|
||||||
|
node_config = G.nodes[node_id].get('config', {})
|
||||||
|
node_data = G.nodes[node_id].get('data', {})
|
||||||
|
node_name = G.nodes[node_id].get('name', '')
|
||||||
|
|
||||||
|
container_info = {
|
||||||
|
'id': node_id,
|
||||||
|
'name': node_name,
|
||||||
|
'config_reagent': node_config.get('reagent', ''),
|
||||||
|
'data_reagent': node_data.get('reagent_name', '')
|
||||||
|
}
|
||||||
|
container_list.append(container_info)
|
||||||
|
debug_print(
|
||||||
|
f" - 容器: {node_id}, 名称: {node_name}, config试剂: {node_config.get('reagent', '')}, data试剂: {node_data.get('reagent_name', '')}")
|
||||||
|
|
||||||
|
debug_print(f"❌ 找不到溶剂 '{solvent}' 对应的容器 😭")
|
||||||
|
debug_print(f"🔍 查找的溶剂: '{solvent}' (小写: '{solvent.lower()}')")
|
||||||
|
debug_print(f"📊 总共发现 {len(container_list)} 个容器")
|
||||||
|
|
||||||
|
raise ValueError(f"找不到溶剂 '{solvent}' 对应的容器")
|
||||||
|
|
||||||
|
|
||||||
|
def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str:
|
||||||
|
"""查找连接到指定容器的搅拌器"""
|
||||||
|
debug_print(f"🔍 查找连接到容器 '{vessel}' 的搅拌器...")
|
||||||
|
|
||||||
|
stirrer_nodes = []
|
||||||
|
for node in G.nodes():
|
||||||
|
node_class = G.nodes[node].get('class', '').lower()
|
||||||
|
if 'stirrer' in node_class:
|
||||||
|
stirrer_nodes.append(node)
|
||||||
|
debug_print(f"📋 发现搅拌器: {node}")
|
||||||
|
|
||||||
|
debug_print(f"📊 共找到 {len(stirrer_nodes)} 个搅拌器")
|
||||||
|
|
||||||
|
# 查找连接到容器的搅拌器
|
||||||
|
for stirrer in stirrer_nodes:
|
||||||
|
if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
|
||||||
|
debug_print(f"✅ 找到连接的搅拌器: {stirrer} 🔗")
|
||||||
|
return stirrer
|
||||||
|
|
||||||
|
# 返回第一个搅拌器
|
||||||
|
if stirrer_nodes:
|
||||||
|
debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个: {stirrer_nodes[0]} 🔄")
|
||||||
|
return stirrer_nodes[0]
|
||||||
|
|
||||||
|
debug_print(f"❌ 未找到任何搅拌器")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def find_solid_dispenser(G: nx.DiGraph) -> str:
|
||||||
|
"""查找固体加样器"""
|
||||||
|
debug_print(f"🔍 查找固体加样器...")
|
||||||
|
|
||||||
|
for node in G.nodes():
|
||||||
|
node_class = G.nodes[node].get('class', '').lower()
|
||||||
|
if 'solid_dispenser' in node_class or 'dispenser' in node_class:
|
||||||
|
debug_print(f"✅ 找到固体加样器: {node} 🥄")
|
||||||
|
return node
|
||||||
|
|
||||||
|
debug_print(f"❌ 未找到固体加样器")
|
||||||
|
return ""
|
||||||
@@ -3,118 +3,14 @@ import networkx as nx
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
from .utils.unit_parser import parse_time_input, parse_volume_input
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def debug_print(message):
|
def debug_print(message):
|
||||||
"""调试输出"""
|
"""调试输出"""
|
||||||
print(f"🧼 [WASH_SOLID] {message}", flush=True)
|
|
||||||
logger.info(f"[WASH_SOLID] {message}")
|
logger.info(f"[WASH_SOLID] {message}")
|
||||||
|
|
||||||
def parse_time_input(time_input: Union[str, float, int]) -> float:
|
|
||||||
"""统一时间解析函数(精简版)"""
|
|
||||||
if not time_input:
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
# 🔢 处理数值输入
|
|
||||||
if isinstance(time_input, (int, float)):
|
|
||||||
result = float(time_input)
|
|
||||||
debug_print(f"⏰ 数值时间: {time_input} → {result}s")
|
|
||||||
return result
|
|
||||||
|
|
||||||
# 📝 处理字符串输入
|
|
||||||
time_str = str(time_input).lower().strip()
|
|
||||||
|
|
||||||
# ❓ 特殊值快速处理
|
|
||||||
special_times = {
|
|
||||||
'?': 60.0, 'unknown': 60.0, 'briefly': 30.0,
|
|
||||||
'quickly': 45.0, 'slowly': 120.0
|
|
||||||
}
|
|
||||||
|
|
||||||
if time_str in special_times:
|
|
||||||
result = special_times[time_str]
|
|
||||||
debug_print(f"🎯 特殊时间: '{time_str}' → {result}s")
|
|
||||||
return result
|
|
||||||
|
|
||||||
# 🔢 数字提取(简化正则)
|
|
||||||
try:
|
|
||||||
# 提取数字
|
|
||||||
numbers = re.findall(r'\d+\.?\d*', time_str)
|
|
||||||
if numbers:
|
|
||||||
value = float(numbers[0])
|
|
||||||
|
|
||||||
# 简化单位判断
|
|
||||||
if any(unit in time_str for unit in ['min', 'm']):
|
|
||||||
result = value * 60.0
|
|
||||||
elif any(unit in time_str for unit in ['h', 'hour']):
|
|
||||||
result = value * 3600.0
|
|
||||||
else:
|
|
||||||
result = value # 默认秒
|
|
||||||
|
|
||||||
debug_print(f"✅ 时间解析: '{time_str}' → {result}s")
|
|
||||||
return result
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
debug_print(f"⚠️ 时间解析失败: '{time_str}',使用默认60s")
|
|
||||||
return 60.0
|
|
||||||
|
|
||||||
def parse_volume_input(volume: Union[float, str], volume_spec: str = "", mass: str = "") -> float:
|
|
||||||
"""统一体积解析函数(精简版)"""
|
|
||||||
debug_print(f"💧 解析体积: volume={volume}, spec='{volume_spec}', mass='{mass}'")
|
|
||||||
|
|
||||||
# 🎯 优先级1:volume_spec(快速映射)
|
|
||||||
if volume_spec:
|
|
||||||
spec_map = {
|
|
||||||
'small': 20.0, 'medium': 50.0, 'large': 100.0,
|
|
||||||
'minimal': 10.0, 'normal': 50.0, 'generous': 150.0
|
|
||||||
}
|
|
||||||
for key, val in spec_map.items():
|
|
||||||
if key in volume_spec.lower():
|
|
||||||
debug_print(f"🎯 规格匹配: '{volume_spec}' → {val}mL")
|
|
||||||
return val
|
|
||||||
|
|
||||||
# 🧮 优先级2:mass转体积(简化:1g=1mL)
|
|
||||||
if mass:
|
|
||||||
try:
|
|
||||||
numbers = re.findall(r'\d+\.?\d*', mass)
|
|
||||||
if numbers:
|
|
||||||
value = float(numbers[0])
|
|
||||||
if 'mg' in mass.lower():
|
|
||||||
result = value / 1000.0
|
|
||||||
elif 'kg' in mass.lower():
|
|
||||||
result = value * 1000.0
|
|
||||||
else:
|
|
||||||
result = value # 默认g
|
|
||||||
debug_print(f"⚖️ 质量转换: {mass} → {result}mL")
|
|
||||||
return result
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 📦 优先级3:volume
|
|
||||||
if volume:
|
|
||||||
if isinstance(volume, (int, float)):
|
|
||||||
result = float(volume)
|
|
||||||
debug_print(f"💧 数值体积: {volume} → {result}mL")
|
|
||||||
return result
|
|
||||||
elif isinstance(volume, str):
|
|
||||||
try:
|
|
||||||
# 提取数字
|
|
||||||
numbers = re.findall(r'\d+\.?\d*', volume)
|
|
||||||
if numbers:
|
|
||||||
value = float(numbers[0])
|
|
||||||
# 简化单位判断
|
|
||||||
if 'l' in volume.lower() and 'ml' not in volume.lower():
|
|
||||||
result = value * 1000.0 # L转mL
|
|
||||||
else:
|
|
||||||
result = value # 默认mL
|
|
||||||
debug_print(f"💧 字符串体积: '{volume}' → {result}mL")
|
|
||||||
return result
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 默认值
|
|
||||||
debug_print(f"⚠️ 体积解析失败,使用默认50mL")
|
|
||||||
return 50.0
|
|
||||||
|
|
||||||
def find_solvent_source(G: nx.DiGraph, solvent: str) -> str:
|
def find_solvent_source(G: nx.DiGraph, solvent: str) -> str:
|
||||||
"""查找溶剂源(精简版)"""
|
"""查找溶剂源(精简版)"""
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
# 定义配置变量和加载函数
|
# 定义配置变量和加载函数
|
||||||
|
import base64
|
||||||
import traceback
|
import traceback
|
||||||
import os
|
import os
|
||||||
import importlib.util
|
import importlib.util
|
||||||
|
from typing import Optional
|
||||||
from unilabos.utils import logger
|
from unilabos.utils import logger
|
||||||
|
|
||||||
|
|
||||||
class BasicConfig:
|
class BasicConfig:
|
||||||
ENV = "pro" # 'test'
|
ENV = "pro" # 'test'
|
||||||
|
ak = ""
|
||||||
|
sk = ""
|
||||||
working_dir = ""
|
working_dir = ""
|
||||||
config_path = ""
|
config_path = ""
|
||||||
is_host_mode = True
|
is_host_mode = True
|
||||||
@@ -17,6 +21,17 @@ class BasicConfig:
|
|||||||
machine_name = "undefined"
|
machine_name = "undefined"
|
||||||
vis_2d_enable = False
|
vis_2d_enable = False
|
||||||
enable_resource_load = True
|
enable_resource_load = True
|
||||||
|
# 通信协议配置
|
||||||
|
communication_protocol = "mqtt" # 支持: "mqtt", "websocket"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def auth_secret(cls):
|
||||||
|
# base64编码
|
||||||
|
if not cls.ak or not cls.sk:
|
||||||
|
return ""
|
||||||
|
target = f"{cls.ak}:{cls.sk}"
|
||||||
|
base64_target = base64.b64encode(target.encode("utf-8")).decode("utf-8")
|
||||||
|
return base64_target
|
||||||
|
|
||||||
|
|
||||||
# MQTT配置
|
# MQTT配置
|
||||||
@@ -38,6 +53,13 @@ class MQConfig:
|
|||||||
key_file = "" # 相对config.py所在目录的路径
|
key_file = "" # 相对config.py所在目录的路径
|
||||||
|
|
||||||
|
|
||||||
|
# WebSocket配置
|
||||||
|
class WSConfig:
|
||||||
|
reconnect_interval = 5 # 重连间隔(秒)
|
||||||
|
max_reconnect_attempts = 999 # 最大重连次数
|
||||||
|
ping_interval = 30 # ping间隔(秒)
|
||||||
|
|
||||||
|
|
||||||
# OSS上传配置
|
# OSS上传配置
|
||||||
class OSSUploadConfig:
|
class OSSUploadConfig:
|
||||||
api_host = ""
|
api_host = ""
|
||||||
@@ -65,7 +87,7 @@ class ROSConfig:
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def _update_config_from_module(module, override_labid: str):
|
def _update_config_from_module(module, override_labid: Optional[str]):
|
||||||
for name, obj in globals().items():
|
for name, obj in globals().items():
|
||||||
if isinstance(obj, type) and name.endswith("Config"):
|
if isinstance(obj, type) and name.endswith("Config"):
|
||||||
if hasattr(module, name) and isinstance(getattr(module, name), type):
|
if hasattr(module, name) and isinstance(getattr(module, name), type):
|
||||||
@@ -74,7 +96,7 @@ def _update_config_from_module(module, override_labid: str):
|
|||||||
setattr(obj, attr, getattr(getattr(module, name), attr))
|
setattr(obj, attr, getattr(getattr(module, name), attr))
|
||||||
# 更新OSS认证
|
# 更新OSS认证
|
||||||
if len(OSSUploadConfig.authorization) == 0:
|
if len(OSSUploadConfig.authorization) == 0:
|
||||||
OSSUploadConfig.authorization = f"lab {MQConfig.lab_id}"
|
OSSUploadConfig.authorization = f"Lab {MQConfig.lab_id}"
|
||||||
# 对 ca_file cert_file key_file 进行初始化
|
# 对 ca_file cert_file key_file 进行初始化
|
||||||
if override_labid:
|
if override_labid:
|
||||||
MQConfig.lab_id = override_labid
|
MQConfig.lab_id = override_labid
|
||||||
@@ -159,7 +181,6 @@ def _update_config_from_env():
|
|||||||
logger.warning(f"[ENV] 解析环境变量 {env_key} 失败: {e}")
|
logger.warning(f"[ENV] 解析环境变量 {env_key} 失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def load_config(config_path=None, override_labid=None):
|
def load_config(config_path=None, override_labid=None):
|
||||||
# 如果提供了配置文件路径,从该文件导入配置
|
# 如果提供了配置文件路径,从该文件导入配置
|
||||||
if config_path:
|
if config_path:
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class MQConfig:
|
|||||||
cert_file = "./lab.crt"
|
cert_file = "./lab.crt"
|
||||||
key_file = "./lab.key"
|
key_file = "./lab.key"
|
||||||
|
|
||||||
|
|
||||||
# HTTP配置
|
# HTTP配置
|
||||||
class HTTPConfig:
|
class HTTPConfig:
|
||||||
remote_addr = "https://uni-lab.bohrium.com/api/v1"
|
remote_addr = "https://uni-lab.bohrium.com/api/v1"
|
||||||
|
|||||||
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
|
||||||
|
|
||||||
############ 第一种写法 ##############
|
############ 第一种写法 ##############
|
||||||
|
|
||||||
|
|||||||
@@ -138,6 +138,8 @@ class LiquidHandlerMiddleware(LiquidHandler):
|
|||||||
offsets: Optional[List[Coordinate]] = None,
|
offsets: Optional[List[Coordinate]] = None,
|
||||||
**backend_kwargs,
|
**backend_kwargs,
|
||||||
):
|
):
|
||||||
|
if not offsets or (isinstance(offsets, list) and len(offsets) != len(use_channels)):
|
||||||
|
offsets = [Coordinate.zero()] * len(use_channels)
|
||||||
if self._simulator:
|
if self._simulator:
|
||||||
return await self._simulate_handler.discard_tips(use_channels, allow_nonzero_volume, offsets, **backend_kwargs)
|
return await self._simulate_handler.discard_tips(use_channels, allow_nonzero_volume, offsets, **backend_kwargs)
|
||||||
return await super().discard_tips(use_channels, allow_nonzero_volume, offsets, **backend_kwargs)
|
return await super().discard_tips(use_channels, allow_nonzero_volume, offsets, **backend_kwargs)
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class PRCXI9300Deck(Deck):
|
|||||||
|
|
||||||
|
|
||||||
class PRCXI9300Container(Plate, TipRack):
|
class PRCXI9300Container(Plate, TipRack):
|
||||||
"""PRCXI 9300 的专用 Deck 类,继承自 Deck。
|
"""PRCXI 9300 的专用 Container 类,继承自 Plate和TipRack。
|
||||||
|
|
||||||
该类定义了 PRCXI 9300 的工作台布局和槽位信息。
|
该类定义了 PRCXI 9300 的工作台布局和槽位信息。
|
||||||
"""
|
"""
|
||||||
|
|||||||
44
unilabos/devices/liquid_handling/prcxi/prcxi_res.py
Normal file
44
unilabos/devices/liquid_handling/prcxi/prcxi_res.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import collections
|
||||||
|
|
||||||
|
from pylabrobot.resources import opentrons_96_tiprack_10ul
|
||||||
|
from pylabrobot.resources.opentrons.plates import corning_96_wellplate_360ul_flat, nest_96_wellplate_2ml_deep
|
||||||
|
|
||||||
|
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300Container, PRCXI9300Trash
|
||||||
|
|
||||||
|
|
||||||
|
def get_well_container(name: str) -> PRCXI9300Container:
|
||||||
|
well_containers = corning_96_wellplate_360ul_flat(name).serialize()
|
||||||
|
plate = PRCXI9300Container(name=name, size_x=50, size_y=50, size_z=10, category="plate",
|
||||||
|
ordering=collections.OrderedDict())
|
||||||
|
plate_serialized = plate.serialize()
|
||||||
|
well_containers.update({k: v for k, v in plate_serialized.items() if k not in ["children"]})
|
||||||
|
new_plate: PRCXI9300Container = PRCXI9300Container.deserialize(well_containers)
|
||||||
|
return new_plate
|
||||||
|
|
||||||
|
def get_tip_rack(name: str) -> PRCXI9300Container:
|
||||||
|
tip_racks = opentrons_96_tiprack_10ul("name").serialize()
|
||||||
|
tip_rack = PRCXI9300Container(name=name, size_x=50, size_y=50, size_z=10, category="tip_rack",
|
||||||
|
ordering=collections.OrderedDict())
|
||||||
|
tip_rack_serialized = tip_rack.serialize()
|
||||||
|
tip_racks.update({k: v for k, v in tip_rack_serialized.items() if k not in ["children"]})
|
||||||
|
new_tip_rack: PRCXI9300Container = PRCXI9300Container.deserialize(tip_racks)
|
||||||
|
return new_tip_rack
|
||||||
|
|
||||||
|
def prcxi_96_wellplate_360ul_flat(name: str):
|
||||||
|
return get_well_container(name)
|
||||||
|
|
||||||
|
def prcxi_opentrons_96_tiprack_10ul(name: str):
|
||||||
|
return get_tip_rack(name)
|
||||||
|
|
||||||
|
def prcxi_trash(name: str = None):
|
||||||
|
return PRCXI9300Trash(name="trash", size_x=50, size_y=50, size_z=10, category="trash")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Example usage
|
||||||
|
test_plate = prcxi_96_wellplate_360ul_flat("test_plate")
|
||||||
|
test_rack = prcxi_opentrons_96_tiprack_10ul("test_rack")
|
||||||
|
tash = prcxi_trash("trash")
|
||||||
|
print(test_plate)
|
||||||
|
print(test_rack)
|
||||||
|
print(tash)
|
||||||
|
# Output will be a dictionary representation of the PRCXI9300Container with well details
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
import time
|
|
||||||
import threading
|
|
||||||
|
|
||||||
|
|
||||||
class MockChiller:
|
|
||||||
def __init__(self, port: str = "MOCK"):
|
|
||||||
self.port = port
|
|
||||||
self._current_temperature: float = 25.0 # 室温开始
|
|
||||||
self._target_temperature: float = 25.0
|
|
||||||
self._status: str = "Idle"
|
|
||||||
self._is_cooling: bool = False
|
|
||||||
self._is_heating: bool = False
|
|
||||||
self._vessel = "Unknown"
|
|
||||||
self._purpose = "Unknown"
|
|
||||||
|
|
||||||
# 模拟温度变化的线程
|
|
||||||
self._temperature_thread = None
|
|
||||||
self._running = True
|
|
||||||
self._temperature_thread = threading.Thread(target=self._temperature_control_loop)
|
|
||||||
self._temperature_thread.daemon = True
|
|
||||||
self._temperature_thread.start()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_temperature(self) -> float:
|
|
||||||
"""当前温度 - 会被自动识别的设备属性"""
|
|
||||||
return self._current_temperature
|
|
||||||
|
|
||||||
@property
|
|
||||||
def target_temperature(self) -> float:
|
|
||||||
"""目标温度"""
|
|
||||||
return self._target_temperature
|
|
||||||
|
|
||||||
@property
|
|
||||||
def status(self) -> str:
|
|
||||||
"""设备状态 - 会被自动识别的设备属性"""
|
|
||||||
return self._status
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_cooling(self) -> bool:
|
|
||||||
"""是否正在冷却"""
|
|
||||||
return self._is_cooling
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_heating(self) -> bool:
|
|
||||||
"""是否正在加热"""
|
|
||||||
return self._is_heating
|
|
||||||
|
|
||||||
@property
|
|
||||||
def vessel(self) -> str:
|
|
||||||
"""当前操作的容器名称"""
|
|
||||||
return self._vessel
|
|
||||||
|
|
||||||
@property
|
|
||||||
def purpose(self) -> str:
|
|
||||||
"""当前操作目的"""
|
|
||||||
return self._purpose
|
|
||||||
|
|
||||||
def heat_chill_start(self, vessel: str, temp: float, purpose: str):
|
|
||||||
"""设置目标温度并记录容器和目的"""
|
|
||||||
self._vessel = str(vessel)
|
|
||||||
self._purpose = str(purpose)
|
|
||||||
self._target_temperature = float(temp)
|
|
||||||
|
|
||||||
diff = self._target_temperature - self._current_temperature
|
|
||||||
if abs(diff) < 0.1:
|
|
||||||
self._status = "At Target Temperature"
|
|
||||||
self._is_cooling = False
|
|
||||||
self._is_heating = False
|
|
||||||
elif diff < 0:
|
|
||||||
self._status = "Cooling"
|
|
||||||
self._is_cooling = True
|
|
||||||
self._is_heating = False
|
|
||||||
else:
|
|
||||||
self._status = "Heating"
|
|
||||||
self._is_heating = True
|
|
||||||
self._is_cooling = False
|
|
||||||
|
|
||||||
self._start_temperature_control()
|
|
||||||
return True
|
|
||||||
|
|
||||||
def heat_chill_stop(self, vessel: str):
|
|
||||||
"""停止加热/制冷"""
|
|
||||||
if vessel != self._vessel:
|
|
||||||
return {"success": False, "status": f"Wrong vessel: expected {self._vessel}, got {vessel}"}
|
|
||||||
|
|
||||||
# 停止温度控制线程,锁定当前温度
|
|
||||||
self._stop_temperature_control()
|
|
||||||
|
|
||||||
# 更新状态
|
|
||||||
self._status = "Stopped"
|
|
||||||
self._is_cooling = False
|
|
||||||
self._is_heating = False
|
|
||||||
|
|
||||||
# 重新启动线程但保持温度
|
|
||||||
self._running = True
|
|
||||||
self._temperature_thread = threading.Thread(target=self._temperature_control_loop)
|
|
||||||
self._temperature_thread.daemon = True
|
|
||||||
self._temperature_thread.start()
|
|
||||||
|
|
||||||
return {"success": True, "status": self._status}
|
|
||||||
|
|
||||||
def _start_temperature_control(self):
|
|
||||||
"""启动温度控制线程"""
|
|
||||||
self._running = True
|
|
||||||
if self._temperature_thread is None or not self._temperature_thread.is_alive():
|
|
||||||
self._temperature_thread = threading.Thread(target=self._temperature_control_loop)
|
|
||||||
self._temperature_thread.daemon = True
|
|
||||||
self._temperature_thread.start()
|
|
||||||
|
|
||||||
def _stop_temperature_control(self):
|
|
||||||
"""停止温度控制"""
|
|
||||||
self._running = False
|
|
||||||
if self._temperature_thread:
|
|
||||||
self._temperature_thread.join(timeout=1.0)
|
|
||||||
|
|
||||||
def _temperature_control_loop(self):
|
|
||||||
"""温度控制循环 - 模拟真实冷却器的温度变化"""
|
|
||||||
while self._running:
|
|
||||||
# 如果状态是 Stopped,不改变温度
|
|
||||||
if self._status == "Stopped":
|
|
||||||
time.sleep(1.0)
|
|
||||||
continue
|
|
||||||
|
|
||||||
temp_diff = self._target_temperature - self._current_temperature
|
|
||||||
|
|
||||||
if abs(temp_diff) < 0.1:
|
|
||||||
self._status = "At Target Temperature"
|
|
||||||
self._is_cooling = False
|
|
||||||
self._is_heating = False
|
|
||||||
elif temp_diff < 0:
|
|
||||||
self._status = "Cooling"
|
|
||||||
self._is_cooling = True
|
|
||||||
self._is_heating = False
|
|
||||||
self._current_temperature -= 0.5
|
|
||||||
else:
|
|
||||||
self._status = "Heating"
|
|
||||||
self._is_heating = True
|
|
||||||
self._is_cooling = False
|
|
||||||
self._current_temperature += 0.3
|
|
||||||
|
|
||||||
time.sleep(1.0)
|
|
||||||
|
|
||||||
def emergency_stop(self):
|
|
||||||
"""紧急停止"""
|
|
||||||
self._status = "Emergency Stop"
|
|
||||||
self._stop_temperature_control()
|
|
||||||
self._is_cooling = False
|
|
||||||
self._is_heating = False
|
|
||||||
|
|
||||||
def get_status_info(self) -> dict:
|
|
||||||
"""获取完整状态信息"""
|
|
||||||
return {
|
|
||||||
"current_temperature": self._current_temperature,
|
|
||||||
"target_temperature": self._target_temperature,
|
|
||||||
"status": self._status,
|
|
||||||
"is_cooling": self._is_cooling,
|
|
||||||
"is_heating": self._is_heating,
|
|
||||||
"vessel": self._vessel,
|
|
||||||
"purpose": self._purpose,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# 用于测试的主函数
|
|
||||||
if __name__ == "__main__":
|
|
||||||
chiller = MockChiller()
|
|
||||||
|
|
||||||
# 测试基本功能
|
|
||||||
print("启动冷却器测试...")
|
|
||||||
print(f"初始状态: {chiller.get_status_info()}")
|
|
||||||
|
|
||||||
# 模拟运行10秒
|
|
||||||
for i in range(10):
|
|
||||||
time.sleep(1)
|
|
||||||
print(f"第{i+1}秒: 当前温度={chiller.current_temperature:.1f}°C, 状态={chiller.status}")
|
|
||||||
|
|
||||||
chiller.emergency_stop()
|
|
||||||
print("测试完成")
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
import time
|
|
||||||
import threading
|
|
||||||
|
|
||||||
|
|
||||||
class MockFilter:
|
|
||||||
def __init__(self, port: str = "MOCK"):
|
|
||||||
# 基本参数初始化
|
|
||||||
self.port = port
|
|
||||||
self._status: str = "Idle"
|
|
||||||
self._is_filtering: bool = False
|
|
||||||
|
|
||||||
# 过滤性能参数
|
|
||||||
self._flow_rate: float = 1.0 # 流速(L/min)
|
|
||||||
self._pressure_drop: float = 0.0 # 压降(Pa)
|
|
||||||
self._filter_life: float = 100.0 # 滤芯寿命(%)
|
|
||||||
|
|
||||||
# 过滤操作参数
|
|
||||||
self._vessel: str = "" # 源容器
|
|
||||||
self._filtrate_vessel: str = "" # 目标容器
|
|
||||||
self._stir: bool = False # 是否搅拌
|
|
||||||
self._stir_speed: float = 0.0 # 搅拌速度
|
|
||||||
self._temperature: float = 25.0 # 温度(℃)
|
|
||||||
self._continue_heatchill: bool = False # 是否继续加热/制冷
|
|
||||||
self._target_volume: float = 0.0 # 目标过滤体积(L)
|
|
||||||
self._filtered_volume: float = 0.0 # 已过滤体积(L)
|
|
||||||
self._progress: float = 0.0 # 过滤进度(%)
|
|
||||||
|
|
||||||
# 线程控制
|
|
||||||
self._filter_thread = None
|
|
||||||
self._running = False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def status(self) -> str:
|
|
||||||
return self._status
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_filtering(self) -> bool:
|
|
||||||
return self._is_filtering
|
|
||||||
|
|
||||||
@property
|
|
||||||
def flow_rate(self) -> float:
|
|
||||||
return self._flow_rate
|
|
||||||
|
|
||||||
@property
|
|
||||||
def pressure_drop(self) -> float:
|
|
||||||
return self._pressure_drop
|
|
||||||
|
|
||||||
@property
|
|
||||||
def filter_life(self) -> float:
|
|
||||||
return self._filter_life
|
|
||||||
# 新增 property
|
|
||||||
@property
|
|
||||||
def vessel(self) -> str:
|
|
||||||
return self._vessel
|
|
||||||
|
|
||||||
@property
|
|
||||||
def filtrate_vessel(self) -> str:
|
|
||||||
return self._filtrate_vessel
|
|
||||||
|
|
||||||
@property
|
|
||||||
def filtered_volume(self) -> float:
|
|
||||||
return self._filtered_volume
|
|
||||||
|
|
||||||
@property
|
|
||||||
def progress(self) -> float:
|
|
||||||
return self._progress
|
|
||||||
|
|
||||||
@property
|
|
||||||
def stir(self) -> bool:
|
|
||||||
return self._stir
|
|
||||||
|
|
||||||
@property
|
|
||||||
def stir_speed(self) -> float:
|
|
||||||
return self._stir_speed
|
|
||||||
|
|
||||||
@property
|
|
||||||
def temperature(self) -> float:
|
|
||||||
return self._temperature
|
|
||||||
|
|
||||||
@property
|
|
||||||
def continue_heatchill(self) -> bool:
|
|
||||||
return self._continue_heatchill
|
|
||||||
|
|
||||||
@property
|
|
||||||
def target_volume(self) -> float:
|
|
||||||
return self._target_volume
|
|
||||||
|
|
||||||
def filter(self, vessel: str, filtrate_vessel: str, stir: bool = False, stir_speed: float = 0.0, temp: float = 25.0, continue_heatchill: bool = False, volume: float = 0.0) -> dict:
|
|
||||||
"""新的过滤操作"""
|
|
||||||
# 停止任何正在进行的过滤
|
|
||||||
if self._is_filtering:
|
|
||||||
self.stop_filtering()
|
|
||||||
# 验证参数
|
|
||||||
if volume <= 0:
|
|
||||||
return {"success": False, "message": "Target volume must be greater than 0"}
|
|
||||||
# 设置新的过滤参数
|
|
||||||
self._vessel = vessel
|
|
||||||
self._filtrate_vessel = filtrate_vessel
|
|
||||||
self._stir = stir
|
|
||||||
self._stir_speed = stir_speed
|
|
||||||
self._temperature = temp
|
|
||||||
self._continue_heatchill = continue_heatchill
|
|
||||||
self._target_volume = volume
|
|
||||||
# 重置过滤状态
|
|
||||||
self._filtered_volume = 0.0
|
|
||||||
self._progress = 0.0
|
|
||||||
self._status = "Starting Filter"
|
|
||||||
# 启动过滤过程
|
|
||||||
self._flow_rate = 1.0 # 设置默认流速
|
|
||||||
self._start_filter_process()
|
|
||||||
|
|
||||||
return {"success": True, "message": "Filter started"}
|
|
||||||
|
|
||||||
def stop_filtering(self):
|
|
||||||
"""停止过滤"""
|
|
||||||
self._status = "Stopping Filter"
|
|
||||||
self._stop_filter_process()
|
|
||||||
self._flow_rate = 0.0
|
|
||||||
self._is_filtering = False
|
|
||||||
self._status = "Stopped"
|
|
||||||
return True
|
|
||||||
|
|
||||||
def replace_filter(self):
|
|
||||||
"""更换滤芯"""
|
|
||||||
self._filter_life = 100.0
|
|
||||||
self._status = "Filter Replaced"
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _start_filter_process(self):
|
|
||||||
"""启动过滤过程线程"""
|
|
||||||
if not self._running:
|
|
||||||
self._running = True
|
|
||||||
self._is_filtering = True
|
|
||||||
self._filter_thread = threading.Thread(target=self._filter_loop)
|
|
||||||
self._filter_thread.daemon = True
|
|
||||||
self._filter_thread.start()
|
|
||||||
|
|
||||||
def _stop_filter_process(self):
|
|
||||||
"""停止过滤过程"""
|
|
||||||
self._running = False
|
|
||||||
if self._filter_thread:
|
|
||||||
self._filter_thread.join(timeout=1.0)
|
|
||||||
|
|
||||||
def _filter_loop(self):
|
|
||||||
"""过滤进程主循环"""
|
|
||||||
update_interval = 1.0 # 更新间隔(秒)
|
|
||||||
|
|
||||||
while self._running and self._is_filtering:
|
|
||||||
try:
|
|
||||||
self._status = "Filtering"
|
|
||||||
|
|
||||||
# 计算这一秒过滤的体积 (L/min -> L/s)
|
|
||||||
volume_increment = (self._flow_rate / 60.0) * update_interval
|
|
||||||
|
|
||||||
# 更新已过滤体积
|
|
||||||
self._filtered_volume += volume_increment
|
|
||||||
|
|
||||||
# 更新进度 (避免除零错误)
|
|
||||||
if self._target_volume > 0:
|
|
||||||
self._progress = min(100.0, (self._filtered_volume / self._target_volume) * 100.0)
|
|
||||||
|
|
||||||
# 更新滤芯寿命 (每过滤1L减少0.5%寿命)
|
|
||||||
self._filter_life = max(0.0, self._filter_life - (volume_increment * 0.5))
|
|
||||||
|
|
||||||
# 更新压降 (根据滤芯寿命和流速动态计算)
|
|
||||||
life_factor = self._filter_life / 100.0 # 将寿命转换为0-1的因子
|
|
||||||
flow_factor = self._flow_rate / 2.0 # 将流速标准化(假设2L/min是标准流速)
|
|
||||||
base_pressure = 100.0 # 基础压降
|
|
||||||
# 压降随滤芯寿命降低而增加,随流速增加而增加
|
|
||||||
self._pressure_drop = base_pressure * (2 - life_factor) * flow_factor
|
|
||||||
|
|
||||||
# 检查是否完成目标体积
|
|
||||||
if self._target_volume > 0 and self._filtered_volume >= self._target_volume:
|
|
||||||
self._status = "Completed"
|
|
||||||
self._progress = 100.0
|
|
||||||
self.stop_filtering()
|
|
||||||
break
|
|
||||||
|
|
||||||
# 检查滤芯寿命
|
|
||||||
if self._filter_life <= 10.0:
|
|
||||||
self._status = "Filter Needs Replacement"
|
|
||||||
|
|
||||||
time.sleep(update_interval)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error in filter loop: {e}")
|
|
||||||
self.emergency_stop()
|
|
||||||
break
|
|
||||||
|
|
||||||
def emergency_stop(self):
|
|
||||||
"""紧急停止"""
|
|
||||||
self._status = "Emergency Stop"
|
|
||||||
self._stop_filter_process()
|
|
||||||
self._is_filtering = False
|
|
||||||
self._flow_rate = 0.0
|
|
||||||
|
|
||||||
def get_status_info(self) -> dict:
|
|
||||||
"""扩展的状态信息"""
|
|
||||||
return {
|
|
||||||
"status": self._status,
|
|
||||||
"is_filtering": self._is_filtering,
|
|
||||||
"flow_rate": self._flow_rate,
|
|
||||||
"pressure_drop": self._pressure_drop,
|
|
||||||
"filter_life": self._filter_life,
|
|
||||||
"vessel": self._vessel,
|
|
||||||
"filtrate_vessel": self._filtrate_vessel,
|
|
||||||
"filtered_volume": self._filtered_volume,
|
|
||||||
"target_volume": self._target_volume,
|
|
||||||
"progress": self._progress,
|
|
||||||
"temperature": self._temperature,
|
|
||||||
"stir": self._stir,
|
|
||||||
"stir_speed": self._stir_speed
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# 用于测试的主函数
|
|
||||||
if __name__ == "__main__":
|
|
||||||
filter_device = MockFilter()
|
|
||||||
|
|
||||||
# 测试基本功能
|
|
||||||
print("启动过滤器测试...")
|
|
||||||
print(f"初始状态: {filter_device.get_status_info()}")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 模拟运行10秒
|
|
||||||
for i in range(10):
|
|
||||||
time.sleep(1)
|
|
||||||
print(
|
|
||||||
f"第{i+1}秒: "
|
|
||||||
f"寿命={filter_device.filter_life:.1f}%, 状态={filter_device.status}"
|
|
||||||
)
|
|
||||||
|
|
||||||
filter_device.emergency_stop()
|
|
||||||
print("测试完成")
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
import time
|
|
||||||
import threading
|
|
||||||
|
|
||||||
class MockHeater:
|
|
||||||
def __init__(self, port: str = "MOCK"):
|
|
||||||
self.port = port
|
|
||||||
self._current_temperature: float = 25.0 # 室温开始
|
|
||||||
self._target_temperature: float = 25.0
|
|
||||||
self._status: str = "Idle"
|
|
||||||
self._is_heating: bool = False
|
|
||||||
self._heating_power: float = 0.0 # 加热功率百分比 0-100
|
|
||||||
self._max_temperature: float = 300.0 # 最大加热温度
|
|
||||||
|
|
||||||
# 新增加的属性
|
|
||||||
self._vessel: str = "Unknown"
|
|
||||||
self._purpose: str = "Unknown"
|
|
||||||
self._stir: bool = False
|
|
||||||
self._stir_speed: float = 0.0
|
|
||||||
|
|
||||||
# 模拟加热过程的线程
|
|
||||||
self._heating_thread = None
|
|
||||||
self._running = True
|
|
||||||
self._heating_thread = threading.Thread(target=self._heating_control_loop)
|
|
||||||
self._heating_thread.daemon = True
|
|
||||||
self._heating_thread.start()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_temperature(self) -> float:
|
|
||||||
"""当前温度 - 会被自动识别的设备属性"""
|
|
||||||
return self._current_temperature
|
|
||||||
|
|
||||||
@property
|
|
||||||
def target_temperature(self) -> float:
|
|
||||||
"""目标温度"""
|
|
||||||
return self._target_temperature
|
|
||||||
|
|
||||||
@property
|
|
||||||
def status(self) -> str:
|
|
||||||
"""设备状态 - 会被自动识别的设备属性"""
|
|
||||||
return self._status
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_heating(self) -> bool:
|
|
||||||
"""是否正在加热"""
|
|
||||||
return self._is_heating
|
|
||||||
|
|
||||||
@property
|
|
||||||
def heating_power(self) -> float:
|
|
||||||
"""加热功率百分比"""
|
|
||||||
return self._heating_power
|
|
||||||
|
|
||||||
@property
|
|
||||||
def max_temperature(self) -> float:
|
|
||||||
"""最大加热温度"""
|
|
||||||
return self._max_temperature
|
|
||||||
|
|
||||||
@property
|
|
||||||
def vessel(self) -> str:
|
|
||||||
"""当前操作的容器名称"""
|
|
||||||
return self._vessel
|
|
||||||
|
|
||||||
@property
|
|
||||||
def purpose(self) -> str:
|
|
||||||
"""操作目的"""
|
|
||||||
return self._purpose
|
|
||||||
|
|
||||||
@property
|
|
||||||
def stir(self) -> bool:
|
|
||||||
"""是否搅拌"""
|
|
||||||
return self._stir
|
|
||||||
|
|
||||||
@property
|
|
||||||
def stir_speed(self) -> float:
|
|
||||||
"""搅拌速度"""
|
|
||||||
return self._stir_speed
|
|
||||||
|
|
||||||
def heat_chill_start(self, vessel: str, temp: float, purpose: str) -> dict:
|
|
||||||
"""开始加热/制冷过程"""
|
|
||||||
self._vessel = str(vessel)
|
|
||||||
self._purpose = str(purpose)
|
|
||||||
self._target_temperature = float(temp)
|
|
||||||
|
|
||||||
diff = self._target_temperature - self._current_temperature
|
|
||||||
if abs(diff) < 0.1:
|
|
||||||
self._status = "At Target Temperature"
|
|
||||||
self._is_heating = False
|
|
||||||
elif diff > 0:
|
|
||||||
self._status = "Heating"
|
|
||||||
self._is_heating = True
|
|
||||||
else:
|
|
||||||
self._status = "Cooling Down"
|
|
||||||
self._is_heating = False
|
|
||||||
|
|
||||||
return {"success": True, "status": self._status}
|
|
||||||
|
|
||||||
def heat_chill_stop(self, vessel: str) -> dict:
|
|
||||||
"""停止加热/制冷"""
|
|
||||||
if vessel != self._vessel:
|
|
||||||
return {"success": False, "status": f"Wrong vessel: expected {self._vessel}, got {vessel}"}
|
|
||||||
|
|
||||||
self._status = "Stopped"
|
|
||||||
self._is_heating = False
|
|
||||||
self._heating_power = 0.0
|
|
||||||
|
|
||||||
return {"success": True, "status": self._status}
|
|
||||||
|
|
||||||
def heat_chill(self, vessel: str, temp: float, time: float,
|
|
||||||
stir: bool = False, stir_speed: float = 0.0,
|
|
||||||
purpose: str = "Unknown") -> dict:
|
|
||||||
"""完整的加热/制冷控制"""
|
|
||||||
self._vessel = str(vessel)
|
|
||||||
self._target_temperature = float(temp)
|
|
||||||
self._purpose = str(purpose)
|
|
||||||
self._stir = stir
|
|
||||||
self._stir_speed = stir_speed
|
|
||||||
|
|
||||||
diff = self._target_temperature - self._current_temperature
|
|
||||||
if abs(diff) < 0.1:
|
|
||||||
self._status = "At Target Temperature"
|
|
||||||
self._is_heating = False
|
|
||||||
elif diff > 0:
|
|
||||||
self._status = "Heating"
|
|
||||||
self._is_heating = True
|
|
||||||
else:
|
|
||||||
self._status = "Cooling Down"
|
|
||||||
self._is_heating = False
|
|
||||||
|
|
||||||
return {"success": True, "status": self._status}
|
|
||||||
|
|
||||||
def set_temperature(self, temperature: float):
|
|
||||||
"""设置目标温度 - 需要在注册表添加的设备动作"""
|
|
||||||
try:
|
|
||||||
temperature = float(temperature)
|
|
||||||
except ValueError:
|
|
||||||
self._status = "Error: Invalid temperature value"
|
|
||||||
return False
|
|
||||||
|
|
||||||
if temperature > self._max_temperature:
|
|
||||||
self._status = f"Error: Temperature exceeds maximum ({self._max_temperature}°C)"
|
|
||||||
return False
|
|
||||||
|
|
||||||
self._target_temperature = temperature
|
|
||||||
self._status = "Setting Temperature"
|
|
||||||
|
|
||||||
# 启动加热控制
|
|
||||||
self._start_heating_control()
|
|
||||||
return True
|
|
||||||
|
|
||||||
def set_heating_power(self, power: float):
|
|
||||||
"""设置加热功率"""
|
|
||||||
try:
|
|
||||||
power = float(power)
|
|
||||||
except ValueError:
|
|
||||||
self._status = "Error: Invalid power value"
|
|
||||||
return False
|
|
||||||
|
|
||||||
self._heating_power = max(0.0, min(100.0, power)) # 限制在0-100%
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _start_heating_control(self):
|
|
||||||
"""启动加热控制线程"""
|
|
||||||
if not self._running:
|
|
||||||
self._running = True
|
|
||||||
self._heating_thread = threading.Thread(target=self._heating_control_loop)
|
|
||||||
self._heating_thread.daemon = True
|
|
||||||
self._heating_thread.start()
|
|
||||||
|
|
||||||
def _stop_heating_control(self):
|
|
||||||
"""停止加热控制"""
|
|
||||||
self._running = False
|
|
||||||
if self._heating_thread:
|
|
||||||
self._heating_thread.join(timeout=1.0)
|
|
||||||
|
|
||||||
def _heating_control_loop(self):
|
|
||||||
"""加热控制循环"""
|
|
||||||
while self._running:
|
|
||||||
# 如果状态是 Stopped,不改变温度
|
|
||||||
if self._status == "Stopped":
|
|
||||||
time.sleep(1.0)
|
|
||||||
continue
|
|
||||||
|
|
||||||
temp_diff = self._target_temperature - self._current_temperature
|
|
||||||
|
|
||||||
if abs(temp_diff) < 0.1:
|
|
||||||
self._status = "At Target Temperature"
|
|
||||||
self._is_heating = False
|
|
||||||
self._heating_power = 10.0
|
|
||||||
elif temp_diff > 0:
|
|
||||||
self._status = "Heating"
|
|
||||||
self._is_heating = True
|
|
||||||
self._heating_power = min(100.0, abs(temp_diff) * 2)
|
|
||||||
self._current_temperature += 0.5
|
|
||||||
else:
|
|
||||||
self._status = "Cooling Down"
|
|
||||||
self._is_heating = False
|
|
||||||
self._heating_power = 0.0
|
|
||||||
self._current_temperature -= 0.2
|
|
||||||
|
|
||||||
time.sleep(1.0)
|
|
||||||
|
|
||||||
def emergency_stop(self):
|
|
||||||
"""紧急停止"""
|
|
||||||
self._status = "Emergency Stop"
|
|
||||||
self._stop_heating_control()
|
|
||||||
self._is_heating = False
|
|
||||||
self._heating_power = 0.0
|
|
||||||
|
|
||||||
def get_status_info(self) -> dict:
|
|
||||||
"""获取完整状态信息"""
|
|
||||||
return {
|
|
||||||
"current_temperature": self._current_temperature,
|
|
||||||
"target_temperature": self._target_temperature,
|
|
||||||
"status": self._status,
|
|
||||||
"is_heating": self._is_heating,
|
|
||||||
"heating_power": self._heating_power,
|
|
||||||
"max_temperature": self._max_temperature,
|
|
||||||
"vessel": self._vessel,
|
|
||||||
"purpose": self._purpose,
|
|
||||||
"stir": self._stir,
|
|
||||||
"stir_speed": self._stir_speed
|
|
||||||
}
|
|
||||||
|
|
||||||
# 用于测试的主函数
|
|
||||||
if __name__ == "__main__":
|
|
||||||
heater = MockHeater()
|
|
||||||
|
|
||||||
print("启动加热器测试...")
|
|
||||||
print(f"初始状态: {heater.get_status_info()}")
|
|
||||||
|
|
||||||
# 设置目标温度为80度
|
|
||||||
heater.set_temperature(80.0)
|
|
||||||
|
|
||||||
# 模拟运行15秒
|
|
||||||
try:
|
|
||||||
for i in range(15):
|
|
||||||
time.sleep(1)
|
|
||||||
status = heater.get_status_info()
|
|
||||||
print(
|
|
||||||
f"\r温度: {status['current_temperature']:.1f}°C / {status['target_temperature']:.1f}°C | "
|
|
||||||
f"功率: {status['heating_power']:.1f}% | 状态: {status['status']}",
|
|
||||||
end=""
|
|
||||||
)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
heater.emergency_stop()
|
|
||||||
print("\n测试被手动停止")
|
|
||||||
|
|
||||||
print("\n测试完成")
|
|
||||||
@@ -1,360 +0,0 @@
|
|||||||
import time
|
|
||||||
import threading
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
class MockPump:
|
|
||||||
def __init__(self, port: str = "MOCK"):
|
|
||||||
self.port = port
|
|
||||||
|
|
||||||
# 设备基本状态属性
|
|
||||||
self._current_device = "MockPump1" # 设备标识符
|
|
||||||
self._status: str = "Idle" # 设备状态:Idle, Running, Error, Stopped
|
|
||||||
self._pump_state: str = "Stopped" # 泵运行状态:Running, Stopped, Paused
|
|
||||||
|
|
||||||
# 流量相关属性
|
|
||||||
self._flow_rate: float = 0.0 # 当前流速 (mL/min)
|
|
||||||
self._target_flow_rate: float = 0.0 # 目标流速 (mL/min)
|
|
||||||
self._max_flow_rate: float = 100.0 # 最大流速 (mL/min)
|
|
||||||
self._total_volume: float = 0.0 # 累计流量 (mL)
|
|
||||||
|
|
||||||
# 压力相关属性
|
|
||||||
self._pressure: float = 0.0 # 当前压力 (bar)
|
|
||||||
self._max_pressure: float = 10.0 # 最大压力 (bar)
|
|
||||||
|
|
||||||
# 运行控制线程
|
|
||||||
self._pump_thread = None
|
|
||||||
self._running = False
|
|
||||||
self._thread_lock = threading.Lock()
|
|
||||||
|
|
||||||
# 新增 PumpTransfer 相关属性
|
|
||||||
self._from_vessel: str = ""
|
|
||||||
self._to_vessel: str = ""
|
|
||||||
self._transfer_volume: float = 0.0
|
|
||||||
self._amount: str = ""
|
|
||||||
self._transfer_time: float = 0.0
|
|
||||||
self._is_viscous: bool = False
|
|
||||||
self._rinsing_solvent: str = ""
|
|
||||||
self._rinsing_volume: float = 0.0
|
|
||||||
self._rinsing_repeats: int = 0
|
|
||||||
self._is_solid: bool = False
|
|
||||||
|
|
||||||
# 时间追踪
|
|
||||||
self._start_time: datetime = None
|
|
||||||
self._time_spent: timedelta = timedelta()
|
|
||||||
self._time_remaining: timedelta = timedelta()
|
|
||||||
|
|
||||||
# ==================== 状态属性 ====================
|
|
||||||
# 这些属性会被Uni-Lab系统自动识别并定时对外广播
|
|
||||||
|
|
||||||
@property
|
|
||||||
def status(self) -> str:
|
|
||||||
return self._status
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_device(self) -> str:
|
|
||||||
"""当前设备标识符"""
|
|
||||||
return self._current_device
|
|
||||||
|
|
||||||
@property
|
|
||||||
def pump_state(self) -> str:
|
|
||||||
return self._pump_state
|
|
||||||
|
|
||||||
@property
|
|
||||||
def flow_rate(self) -> float:
|
|
||||||
return self._flow_rate
|
|
||||||
|
|
||||||
@property
|
|
||||||
def target_flow_rate(self) -> float:
|
|
||||||
return self._target_flow_rate
|
|
||||||
|
|
||||||
@property
|
|
||||||
def pressure(self) -> float:
|
|
||||||
return self._pressure
|
|
||||||
|
|
||||||
@property
|
|
||||||
def total_volume(self) -> float:
|
|
||||||
return self._total_volume
|
|
||||||
|
|
||||||
@property
|
|
||||||
def max_flow_rate(self) -> float:
|
|
||||||
return self._max_flow_rate
|
|
||||||
|
|
||||||
@property
|
|
||||||
def max_pressure(self) -> float:
|
|
||||||
return self._max_pressure
|
|
||||||
|
|
||||||
# 添加新的属性访问器
|
|
||||||
@property
|
|
||||||
def from_vessel(self) -> str:
|
|
||||||
return self._from_vessel
|
|
||||||
|
|
||||||
@property
|
|
||||||
def to_vessel(self) -> str:
|
|
||||||
return self._to_vessel
|
|
||||||
|
|
||||||
@property
|
|
||||||
def transfer_volume(self) -> float:
|
|
||||||
return self._transfer_volume
|
|
||||||
|
|
||||||
@property
|
|
||||||
def amount(self) -> str:
|
|
||||||
return self._amount
|
|
||||||
|
|
||||||
@property
|
|
||||||
def transfer_time(self) -> float:
|
|
||||||
return self._transfer_time
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_viscous(self) -> bool:
|
|
||||||
return self._is_viscous
|
|
||||||
|
|
||||||
@property
|
|
||||||
def rinsing_solvent(self) -> str:
|
|
||||||
return self._rinsing_solvent
|
|
||||||
|
|
||||||
@property
|
|
||||||
def rinsing_volume(self) -> float:
|
|
||||||
return self._rinsing_volume
|
|
||||||
|
|
||||||
@property
|
|
||||||
def rinsing_repeats(self) -> int:
|
|
||||||
return self._rinsing_repeats
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_solid(self) -> bool:
|
|
||||||
return self._is_solid
|
|
||||||
|
|
||||||
# 修改这两个属性装饰器
|
|
||||||
@property
|
|
||||||
def time_spent(self) -> float:
|
|
||||||
"""已用时间(秒)"""
|
|
||||||
if isinstance(self._time_spent, timedelta):
|
|
||||||
return self._time_spent.total_seconds()
|
|
||||||
return float(self._time_spent)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def time_remaining(self) -> float:
|
|
||||||
"""剩余时间(秒)"""
|
|
||||||
if isinstance(self._time_remaining, timedelta):
|
|
||||||
return self._time_remaining.total_seconds()
|
|
||||||
return float(self._time_remaining)
|
|
||||||
|
|
||||||
# ==================== 设备控制方法 ====================
|
|
||||||
# 这些方法需要在注册表中添加,会作为ActionServer接受控制指令
|
|
||||||
def pump_transfer(self, from_vessel: str, to_vessel: str, volume: float,
|
|
||||||
amount: str = "", time: float = 0.0, viscous: bool = False,
|
|
||||||
rinsing_solvent: str = "", rinsing_volume: float = 0.0,
|
|
||||||
rinsing_repeats: int = 0, solid: bool = False) -> dict:
|
|
||||||
"""Execute pump transfer operation"""
|
|
||||||
# Stop any existing operation first
|
|
||||||
self._stop_pump_operation()
|
|
||||||
|
|
||||||
# Set transfer parameters
|
|
||||||
self._from_vessel = from_vessel
|
|
||||||
self._to_vessel = to_vessel
|
|
||||||
self._transfer_volume = float(volume)
|
|
||||||
self._amount = amount
|
|
||||||
self._transfer_time = float(time)
|
|
||||||
self._is_viscous = viscous
|
|
||||||
self._rinsing_solvent = rinsing_solvent
|
|
||||||
self._rinsing_volume = float(rinsing_volume)
|
|
||||||
self._rinsing_repeats = int(rinsing_repeats)
|
|
||||||
self._is_solid = solid
|
|
||||||
|
|
||||||
# Calculate flow rate
|
|
||||||
if self._transfer_time > 0 and self._transfer_volume > 0:
|
|
||||||
self._target_flow_rate = (self._transfer_volume / self._transfer_time) * 60.0
|
|
||||||
else:
|
|
||||||
self._target_flow_rate = 10.0 if not self._is_viscous else 5.0
|
|
||||||
|
|
||||||
# Reset timers and counters
|
|
||||||
self._start_time = datetime.now()
|
|
||||||
self._time_spent = timedelta()
|
|
||||||
self._time_remaining = timedelta(seconds=self._transfer_time)
|
|
||||||
self._total_volume = 0.0
|
|
||||||
self._flow_rate = 0.0
|
|
||||||
|
|
||||||
# Start pump operation
|
|
||||||
self._pump_state = "Running"
|
|
||||||
self._status = "Starting Transfer"
|
|
||||||
self._running = True
|
|
||||||
|
|
||||||
# Start pump operation thread
|
|
||||||
self._pump_thread = threading.Thread(target=self._pump_operation_loop)
|
|
||||||
self._pump_thread.daemon = True
|
|
||||||
self._pump_thread.start()
|
|
||||||
|
|
||||||
# Wait briefly to ensure thread starts
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"status": self._status,
|
|
||||||
"current_device": self._current_device,
|
|
||||||
"time_spent": 0.0,
|
|
||||||
"time_remaining": float(self._transfer_time)
|
|
||||||
}
|
|
||||||
|
|
||||||
def pause_pump(self) -> str:
|
|
||||||
|
|
||||||
if self._pump_state != "Running":
|
|
||||||
self._status = "Error: Pump not running"
|
|
||||||
return "Error"
|
|
||||||
|
|
||||||
self._pump_state = "Paused"
|
|
||||||
self._status = "Pump Paused"
|
|
||||||
self._stop_pump_operation()
|
|
||||||
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
def resume_pump(self) -> str:
|
|
||||||
|
|
||||||
if self._pump_state != "Paused":
|
|
||||||
self._status = "Error: Pump not paused"
|
|
||||||
return "Error"
|
|
||||||
|
|
||||||
self._pump_state = "Running"
|
|
||||||
self._status = "Resuming Pump"
|
|
||||||
self._start_pump_operation()
|
|
||||||
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
def reset_volume_counter(self) -> str:
|
|
||||||
self._total_volume = 0.0
|
|
||||||
self._status = "Volume counter reset"
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
def emergency_stop(self) -> str:
|
|
||||||
self._status = "Emergency Stop"
|
|
||||||
self._pump_state = "Stopped"
|
|
||||||
self._stop_pump_operation()
|
|
||||||
self._flow_rate = 0.0
|
|
||||||
self._pressure = 0.0
|
|
||||||
self._target_flow_rate = 0.0
|
|
||||||
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
# ==================== 内部控制方法 ====================
|
|
||||||
|
|
||||||
def _start_pump_operation(self):
|
|
||||||
with self._thread_lock:
|
|
||||||
if not self._running:
|
|
||||||
self._running = True
|
|
||||||
self._pump_thread = threading.Thread(target=self._pump_operation_loop)
|
|
||||||
self._pump_thread.daemon = True
|
|
||||||
self._pump_thread.start()
|
|
||||||
|
|
||||||
def _stop_pump_operation(self):
|
|
||||||
with self._thread_lock:
|
|
||||||
self._running = False
|
|
||||||
if self._pump_thread and self._pump_thread.is_alive():
|
|
||||||
self._pump_thread.join(timeout=2.0)
|
|
||||||
|
|
||||||
def _pump_operation_loop(self):
|
|
||||||
"""泵运行主循环"""
|
|
||||||
print("Pump operation loop started") # Debug print
|
|
||||||
|
|
||||||
while self._running and self._pump_state == "Running":
|
|
||||||
try:
|
|
||||||
# Calculate flow rate adjustment
|
|
||||||
flow_diff = self._target_flow_rate - self._flow_rate
|
|
||||||
|
|
||||||
# Adjust flow rate more aggressively (50% of difference)
|
|
||||||
adjustment = flow_diff * 0.5
|
|
||||||
self._flow_rate += adjustment
|
|
||||||
|
|
||||||
# Ensure flow rate is within bounds
|
|
||||||
self._flow_rate = max(0.1, min(self._max_flow_rate, self._flow_rate))
|
|
||||||
|
|
||||||
# Update status based on flow rate
|
|
||||||
if abs(flow_diff) < 0.1:
|
|
||||||
self._status = "Running at Target Flow Rate"
|
|
||||||
else:
|
|
||||||
self._status = "Adjusting Flow Rate"
|
|
||||||
|
|
||||||
# Calculate volume increment
|
|
||||||
volume_increment = (self._flow_rate / 60.0) # mL/s
|
|
||||||
self._total_volume += volume_increment
|
|
||||||
|
|
||||||
# Update time tracking
|
|
||||||
self._time_spent = datetime.now() - self._start_time
|
|
||||||
if self._transfer_time > 0:
|
|
||||||
remaining = self._transfer_time - self._time_spent.total_seconds()
|
|
||||||
self._time_remaining = timedelta(seconds=max(0, remaining))
|
|
||||||
|
|
||||||
# Check completion
|
|
||||||
if self._total_volume >= self._transfer_volume:
|
|
||||||
self._status = "Transfer Completed"
|
|
||||||
self._pump_state = "Stopped"
|
|
||||||
self._running = False
|
|
||||||
break
|
|
||||||
|
|
||||||
# Update pressure
|
|
||||||
self._pressure = (self._flow_rate / self._max_flow_rate) * self._max_pressure
|
|
||||||
|
|
||||||
print(f"Debug - Flow: {self._flow_rate:.1f}, Volume: {self._total_volume:.1f}") # Debug print
|
|
||||||
|
|
||||||
time.sleep(1.0)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error in pump operation: {str(e)}")
|
|
||||||
self._status = "Error in pump operation"
|
|
||||||
self._pump_state = "Stopped"
|
|
||||||
self._running = False
|
|
||||||
break
|
|
||||||
|
|
||||||
def get_status_info(self) -> dict:
|
|
||||||
"""
|
|
||||||
获取完整的设备状态信息
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: 包含所有设备状态的字典
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
"status": self._status,
|
|
||||||
"pump_state": self._pump_state,
|
|
||||||
"flow_rate": self._flow_rate,
|
|
||||||
"target_flow_rate": self._target_flow_rate,
|
|
||||||
"pressure": self._pressure,
|
|
||||||
"total_volume": self._total_volume,
|
|
||||||
"max_flow_rate": self._max_flow_rate,
|
|
||||||
"max_pressure": self._max_pressure,
|
|
||||||
"current_device": self._current_device,
|
|
||||||
"from_vessel": self._from_vessel,
|
|
||||||
"to_vessel": self._to_vessel,
|
|
||||||
"transfer_volume": self._transfer_volume,
|
|
||||||
"amount": self._amount,
|
|
||||||
"transfer_time": self._transfer_time,
|
|
||||||
"is_viscous": self._is_viscous,
|
|
||||||
"rinsing_solvent": self._rinsing_solvent,
|
|
||||||
"rinsing_volume": self._rinsing_volume,
|
|
||||||
"rinsing_repeats": self._rinsing_repeats,
|
|
||||||
"is_solid": self._is_solid,
|
|
||||||
"time_spent": self._time_spent.total_seconds(),
|
|
||||||
"time_remaining": self._time_remaining.total_seconds()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# 用于测试的主函数
|
|
||||||
if __name__ == "__main__":
|
|
||||||
pump = MockPump()
|
|
||||||
|
|
||||||
# 测试基本功能
|
|
||||||
print("启动泵设备测试...")
|
|
||||||
print(f"初始状态: {pump.get_status_info()}")
|
|
||||||
|
|
||||||
# 设置流速并启动
|
|
||||||
pump.set_flow_rate(50.0)
|
|
||||||
pump.start_pump()
|
|
||||||
|
|
||||||
# 模拟运行10秒
|
|
||||||
for i in range(10):
|
|
||||||
time.sleep(1)
|
|
||||||
print(f"第{i+1}秒: 流速={pump.flow_rate:.1f}mL/min, 压力={pump.pressure:.2f}bar, 状态={pump.status}")
|
|
||||||
|
|
||||||
# 测试方向切换
|
|
||||||
print("切换泵方向...")
|
|
||||||
|
|
||||||
|
|
||||||
pump.emergency_stop()
|
|
||||||
print("测试完成")
|
|
||||||
@@ -1,390 +0,0 @@
|
|||||||
import time
|
|
||||||
import threading
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
class MockRotavap:
|
|
||||||
"""
|
|
||||||
模拟旋转蒸发器设备类
|
|
||||||
|
|
||||||
这个类模拟了一个实验室旋转蒸发器的行为,包括旋转控制、
|
|
||||||
真空泵控制、温度控制等功能。参考了现有的 RotavapOne 实现。
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, port: str = "MOCK"):
|
|
||||||
"""
|
|
||||||
初始化MockRotavap实例
|
|
||||||
|
|
||||||
Args:
|
|
||||||
port (str): 设备端口,默认为"MOCK"表示模拟设备
|
|
||||||
"""
|
|
||||||
self.port = port
|
|
||||||
|
|
||||||
# 设备基本状态属性
|
|
||||||
self._status: str = "Idle" # 设备状态:Idle, Running, Error, Stopped
|
|
||||||
|
|
||||||
# 旋转相关属性
|
|
||||||
self._rotate_state: str = "Stopped" # 旋转状态:Running, Stopped
|
|
||||||
self._rotate_time: float = 0.0 # 旋转剩余时间 (秒)
|
|
||||||
self._rotate_speed: float = 0.0 # 旋转速度 (rpm)
|
|
||||||
self._max_rotate_speed: float = 300.0 # 最大旋转速度 (rpm)
|
|
||||||
|
|
||||||
# 真空泵相关属性
|
|
||||||
self._pump_state: str = "Stopped" # 泵状态:Running, Stopped
|
|
||||||
self._pump_time: float = 0.0 # 泵剩余时间 (秒)
|
|
||||||
self._vacuum_level: float = 0.0 # 真空度 (mbar)
|
|
||||||
self._target_vacuum: float = 50.0 # 目标真空度 (mbar)
|
|
||||||
|
|
||||||
# 温度相关属性
|
|
||||||
self._temperature: float = 25.0 # 水浴温度 (°C)
|
|
||||||
self._target_temperature: float = 25.0 # 目标温度 (°C)
|
|
||||||
self._max_temperature: float = 180.0 # 最大温度 (°C)
|
|
||||||
|
|
||||||
# 运行控制线程
|
|
||||||
self._operation_thread = None
|
|
||||||
self._running = False
|
|
||||||
self._thread_lock = threading.Lock()
|
|
||||||
|
|
||||||
# 操作成功标志
|
|
||||||
self.success: str = "True" # 使用字符串而不是布尔值
|
|
||||||
|
|
||||||
# ==================== 状态属性 ====================
|
|
||||||
# 这些属性会被Uni-Lab系统自动识别并定时对外广播
|
|
||||||
|
|
||||||
@property
|
|
||||||
def status(self) -> str:
|
|
||||||
return self._status
|
|
||||||
|
|
||||||
@property
|
|
||||||
def rotate_state(self) -> str:
|
|
||||||
return self._rotate_state
|
|
||||||
|
|
||||||
@property
|
|
||||||
def rotate_time(self) -> float:
|
|
||||||
return self._rotate_time
|
|
||||||
|
|
||||||
@property
|
|
||||||
def rotate_speed(self) -> float:
|
|
||||||
return self._rotate_speed
|
|
||||||
|
|
||||||
@property
|
|
||||||
def pump_state(self) -> str:
|
|
||||||
return self._pump_state
|
|
||||||
|
|
||||||
@property
|
|
||||||
def pump_time(self) -> float:
|
|
||||||
return self._pump_time
|
|
||||||
|
|
||||||
@property
|
|
||||||
def vacuum_level(self) -> float:
|
|
||||||
return self._vacuum_level
|
|
||||||
|
|
||||||
@property
|
|
||||||
def temperature(self) -> float:
|
|
||||||
return self._temperature
|
|
||||||
|
|
||||||
@property
|
|
||||||
def target_temperature(self) -> float:
|
|
||||||
return self._target_temperature
|
|
||||||
|
|
||||||
# ==================== 设备控制方法 ====================
|
|
||||||
# 这些方法需要在注册表中添加,会作为ActionServer接受控制指令
|
|
||||||
|
|
||||||
def set_timer(self, command: str) -> str:
|
|
||||||
"""
|
|
||||||
设置定时器 - 兼容现有RotavapOne接口
|
|
||||||
|
|
||||||
Args:
|
|
||||||
command (str): JSON格式的命令字符串,包含rotate_time和pump_time
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 操作结果状态 ("Success", "Error")
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
timer = json.loads(command)
|
|
||||||
rotate_time = timer.get("rotate_time", 0)
|
|
||||||
pump_time = timer.get("pump_time", 0)
|
|
||||||
|
|
||||||
self.success = "False"
|
|
||||||
self._rotate_time = float(rotate_time)
|
|
||||||
self._pump_time = float(pump_time)
|
|
||||||
self.success = "True"
|
|
||||||
|
|
||||||
self._status = "Timer Set"
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
except (json.JSONDecodeError, ValueError, KeyError) as e:
|
|
||||||
self._status = f"Error: Invalid command format - {str(e)}"
|
|
||||||
self.success = "False"
|
|
||||||
return "Error"
|
|
||||||
|
|
||||||
def set_rotate_time(self, time_seconds: float) -> str:
|
|
||||||
"""
|
|
||||||
设置旋转时间
|
|
||||||
|
|
||||||
Args:
|
|
||||||
time_seconds (float): 旋转时间 (秒)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 操作结果状态 ("Success", "Error")
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.success = "False"
|
|
||||||
self._rotate_time = max(0.0, float(time_seconds))
|
|
||||||
self.success = "True"
|
|
||||||
self._status = "Rotate time set"
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
def set_pump_time(self, time_seconds: float) -> str:
|
|
||||||
"""
|
|
||||||
设置泵时间
|
|
||||||
|
|
||||||
Args:
|
|
||||||
time_seconds (float): 泵时间 (秒)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 操作结果状态 ("Success", "Error")
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.success = "False"
|
|
||||||
self._pump_time = max(0.0, float(time_seconds))
|
|
||||||
self.success = "True"
|
|
||||||
self._status = "Pump time set"
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
def set_rotate_speed(self, speed: float) -> str:
|
|
||||||
"""
|
|
||||||
设置旋转速度
|
|
||||||
|
|
||||||
Args:
|
|
||||||
speed (float): 旋转速度 (rpm)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 操作结果状态 ("Success", "Error")
|
|
||||||
"""
|
|
||||||
|
|
||||||
if speed < 0 or speed > self._max_rotate_speed:
|
|
||||||
self._status = f"Error: Speed out of range (0-{self._max_rotate_speed})"
|
|
||||||
return "Error"
|
|
||||||
|
|
||||||
self._rotate_speed = speed
|
|
||||||
self._status = "Rotate speed set"
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
def set_temperature(self, temperature: float) -> str:
|
|
||||||
"""
|
|
||||||
设置水浴温度
|
|
||||||
|
|
||||||
Args:
|
|
||||||
temperature (float): 目标温度 (°C)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 操作结果状态 ("Success", "Error")
|
|
||||||
"""
|
|
||||||
|
|
||||||
if temperature < 0 or temperature > self._max_temperature:
|
|
||||||
self._status = f"Error: Temperature out of range (0-{self._max_temperature})"
|
|
||||||
return "Error"
|
|
||||||
|
|
||||||
self._target_temperature = temperature
|
|
||||||
self._status = "Temperature set"
|
|
||||||
|
|
||||||
# 启动操作线程以开始温度控制
|
|
||||||
self._start_operation()
|
|
||||||
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
def start_rotation(self) -> str:
|
|
||||||
"""
|
|
||||||
启动旋转
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 操作结果状态 ("Success", "Error")
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self._rotate_time <= 0:
|
|
||||||
self._status = "Error: No rotate time set"
|
|
||||||
return "Error"
|
|
||||||
|
|
||||||
self._rotate_state = "Running"
|
|
||||||
self._status = "Rotation started"
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
def start_pump(self) -> str:
|
|
||||||
"""
|
|
||||||
启动真空泵
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 操作结果状态 ("Success", "Error")
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self._pump_time <= 0:
|
|
||||||
self._status = "Error: No pump time set"
|
|
||||||
return "Error"
|
|
||||||
|
|
||||||
self._pump_state = "Running"
|
|
||||||
self._status = "Pump started"
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
def stop_all_operations(self) -> str:
|
|
||||||
"""
|
|
||||||
停止所有操作
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 操作结果状态 ("Success", "Error")
|
|
||||||
"""
|
|
||||||
self._rotate_state = "Stopped"
|
|
||||||
self._pump_state = "Stopped"
|
|
||||||
self._stop_operation()
|
|
||||||
self._rotate_time = 0.0
|
|
||||||
self._pump_time = 0.0
|
|
||||||
self._vacuum_level = 0.0
|
|
||||||
self._status = "All operations stopped"
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
def emergency_stop(self) -> str:
|
|
||||||
"""
|
|
||||||
紧急停止
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 操作结果状态 ("Success", "Error")
|
|
||||||
"""
|
|
||||||
self._status = "Emergency Stop"
|
|
||||||
self.stop_all_operations()
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
# ==================== 内部控制方法 ====================
|
|
||||||
|
|
||||||
def _start_operation(self):
|
|
||||||
"""
|
|
||||||
启动操作线程
|
|
||||||
|
|
||||||
这个方法启动一个后台线程来模拟旋蒸的实际运行过程。
|
|
||||||
"""
|
|
||||||
with self._thread_lock:
|
|
||||||
if not self._running:
|
|
||||||
self._running = True
|
|
||||||
self._operation_thread = threading.Thread(target=self._operation_loop)
|
|
||||||
self._operation_thread.daemon = True
|
|
||||||
self._operation_thread.start()
|
|
||||||
|
|
||||||
def _stop_operation(self):
|
|
||||||
"""
|
|
||||||
停止操作线程
|
|
||||||
|
|
||||||
安全地停止后台运行线程并等待其完成。
|
|
||||||
"""
|
|
||||||
with self._thread_lock:
|
|
||||||
self._running = False
|
|
||||||
if self._operation_thread and self._operation_thread.is_alive():
|
|
||||||
self._operation_thread.join(timeout=2.0)
|
|
||||||
|
|
||||||
def _operation_loop(self):
|
|
||||||
"""
|
|
||||||
操作主循环
|
|
||||||
|
|
||||||
这个方法在后台线程中运行,模拟真实旋蒸的工作过程:
|
|
||||||
1. 时间倒计时
|
|
||||||
2. 温度控制
|
|
||||||
3. 真空度控制
|
|
||||||
4. 状态更新
|
|
||||||
"""
|
|
||||||
while self._running:
|
|
||||||
try:
|
|
||||||
# 处理旋转时间倒计时
|
|
||||||
if self._rotate_time > 0:
|
|
||||||
self._rotate_state = "Running"
|
|
||||||
self._rotate_time = max(0.0, self._rotate_time - 1.0)
|
|
||||||
else:
|
|
||||||
self._rotate_state = "Stopped"
|
|
||||||
|
|
||||||
# 处理泵时间倒计时
|
|
||||||
if self._pump_time > 0:
|
|
||||||
self._pump_state = "Running"
|
|
||||||
self._pump_time = max(0.0, self._pump_time - 1.0)
|
|
||||||
# 模拟真空度变化
|
|
||||||
if self._vacuum_level > self._target_vacuum:
|
|
||||||
self._vacuum_level = max(self._target_vacuum, self._vacuum_level - 5.0)
|
|
||||||
else:
|
|
||||||
self._pump_state = "Stopped"
|
|
||||||
# 真空度逐渐回升
|
|
||||||
self._vacuum_level = min(1013.25, self._vacuum_level + 2.0)
|
|
||||||
|
|
||||||
# 模拟温度控制
|
|
||||||
temp_diff = self._target_temperature - self._temperature
|
|
||||||
if abs(temp_diff) > 0.5:
|
|
||||||
if temp_diff > 0:
|
|
||||||
self._temperature += min(1.0, temp_diff * 0.1)
|
|
||||||
else:
|
|
||||||
self._temperature += max(-1.0, temp_diff * 0.1)
|
|
||||||
|
|
||||||
# 更新整体状态
|
|
||||||
if self._rotate_state == "Running" or self._pump_state == "Running":
|
|
||||||
self._status = "Operating"
|
|
||||||
elif self._rotate_time > 0 or self._pump_time > 0:
|
|
||||||
self._status = "Ready"
|
|
||||||
else:
|
|
||||||
self._status = "Idle"
|
|
||||||
|
|
||||||
# 等待1秒后继续下一次循环
|
|
||||||
time.sleep(1.0)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._status = f"Error in operation: {str(e)}"
|
|
||||||
break
|
|
||||||
|
|
||||||
# 循环结束时的清理工作
|
|
||||||
self._status = "Idle"
|
|
||||||
|
|
||||||
def get_status_info(self) -> dict:
|
|
||||||
"""
|
|
||||||
获取完整的设备状态信息
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: 包含所有设备状态的字典
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
"status": self._status,
|
|
||||||
"rotate_state": self._rotate_state,
|
|
||||||
"rotate_time": self._rotate_time,
|
|
||||||
"rotate_speed": self._rotate_speed,
|
|
||||||
"pump_state": self._pump_state,
|
|
||||||
"pump_time": self._pump_time,
|
|
||||||
"vacuum_level": self._vacuum_level,
|
|
||||||
"temperature": self._temperature,
|
|
||||||
"target_temperature": self._target_temperature,
|
|
||||||
"success": self.success,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# 用于测试的主函数
|
|
||||||
if __name__ == "__main__":
|
|
||||||
rotavap = MockRotavap()
|
|
||||||
|
|
||||||
# 测试基本功能
|
|
||||||
print("启动旋转蒸发器测试...")
|
|
||||||
print(f"初始状态: {rotavap.get_status_info()}")
|
|
||||||
|
|
||||||
# 设置定时器
|
|
||||||
timer_command = '{"rotate_time": 300, "pump_time": 600}'
|
|
||||||
rotavap.set_timer(timer_command)
|
|
||||||
|
|
||||||
# 设置温度和转速
|
|
||||||
rotavap.set_temperature(60.0)
|
|
||||||
rotavap.set_rotate_speed(120.0)
|
|
||||||
|
|
||||||
# 启动操作
|
|
||||||
rotavap.start_rotation()
|
|
||||||
rotavap.start_pump()
|
|
||||||
|
|
||||||
# 模拟运行10秒
|
|
||||||
for i in range(10):
|
|
||||||
time.sleep(1)
|
|
||||||
print(
|
|
||||||
f"第{i+1}秒: 旋转={rotavap.rotate_time:.0f}s, 泵={rotavap.pump_time:.0f}s, "
|
|
||||||
f"温度={rotavap.temperature:.1f}°C, 真空={rotavap.vacuum_level:.1f}mbar"
|
|
||||||
)
|
|
||||||
|
|
||||||
rotavap.emergency_stop()
|
|
||||||
print("测试完成")
|
|
||||||
@@ -1,399 +0,0 @@
|
|||||||
import time
|
|
||||||
import threading
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
class MockSeparator:
|
|
||||||
def __init__(self, port: str = "MOCK"):
|
|
||||||
self.port = port
|
|
||||||
|
|
||||||
# 基本状态属性
|
|
||||||
self._status: str = "Idle" # 当前总体状态
|
|
||||||
self._valve_state: str = "Closed" # 阀门状态:Open 或 Closed
|
|
||||||
self._settling_time: float = 0.0 # 静置时间(秒)
|
|
||||||
|
|
||||||
# 搅拌相关属性
|
|
||||||
self._shake_time: float = 0.0 # 剩余摇摆时间(秒)
|
|
||||||
self._shake_status: str = "Not Shaking" # 摇摆状态
|
|
||||||
|
|
||||||
# 用于后台模拟 shake 动作
|
|
||||||
self._operation_thread = None
|
|
||||||
self._thread_lock = threading.Lock()
|
|
||||||
self._running = False
|
|
||||||
|
|
||||||
# Separate action 相关属性
|
|
||||||
self._current_device: str = "MockSeparator1"
|
|
||||||
self._purpose: str = "" # wash or extract
|
|
||||||
self._product_phase: str = "" # top or bottom
|
|
||||||
self._from_vessel: str = ""
|
|
||||||
self._separation_vessel: str = ""
|
|
||||||
self._to_vessel: str = ""
|
|
||||||
self._waste_phase_to_vessel: str = ""
|
|
||||||
self._solvent: str = ""
|
|
||||||
self._solvent_volume: float = 0.0
|
|
||||||
self._through: str = ""
|
|
||||||
self._repeats: int = 1
|
|
||||||
self._stir_time: float = 0.0
|
|
||||||
self._stir_speed: float = 0.0
|
|
||||||
self._time_spent = timedelta()
|
|
||||||
self._time_remaining = timedelta()
|
|
||||||
self._start_time = datetime.now() # 添加这一行
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_device(self) -> str:
|
|
||||||
return self._current_device
|
|
||||||
|
|
||||||
@property
|
|
||||||
def purpose(self) -> str:
|
|
||||||
return self._purpose
|
|
||||||
|
|
||||||
@property
|
|
||||||
def valve_state(self) -> str:
|
|
||||||
return self._valve_state
|
|
||||||
|
|
||||||
@property
|
|
||||||
def settling_time(self) -> float:
|
|
||||||
return self._settling_time
|
|
||||||
|
|
||||||
@property
|
|
||||||
def status(self) -> str:
|
|
||||||
return self._status
|
|
||||||
|
|
||||||
@property
|
|
||||||
def shake_time(self) -> float:
|
|
||||||
with self._thread_lock:
|
|
||||||
return self._shake_time
|
|
||||||
|
|
||||||
@property
|
|
||||||
def shake_status(self) -> str:
|
|
||||||
with self._thread_lock:
|
|
||||||
return self._shake_status
|
|
||||||
|
|
||||||
@property
|
|
||||||
def product_phase(self) -> str:
|
|
||||||
return self._product_phase
|
|
||||||
|
|
||||||
@property
|
|
||||||
def from_vessel(self) -> str:
|
|
||||||
return self._from_vessel
|
|
||||||
|
|
||||||
@property
|
|
||||||
def separation_vessel(self) -> str:
|
|
||||||
return self._separation_vessel
|
|
||||||
|
|
||||||
@property
|
|
||||||
def to_vessel(self) -> str:
|
|
||||||
return self._to_vessel
|
|
||||||
|
|
||||||
@property
|
|
||||||
def waste_phase_to_vessel(self) -> str:
|
|
||||||
return self._waste_phase_to_vessel
|
|
||||||
|
|
||||||
@property
|
|
||||||
def solvent(self) -> str:
|
|
||||||
return self._solvent
|
|
||||||
|
|
||||||
@property
|
|
||||||
def solvent_volume(self) -> float:
|
|
||||||
return self._solvent_volume
|
|
||||||
|
|
||||||
@property
|
|
||||||
def through(self) -> str:
|
|
||||||
return self._through
|
|
||||||
|
|
||||||
@property
|
|
||||||
def repeats(self) -> int:
|
|
||||||
return self._repeats
|
|
||||||
|
|
||||||
@property
|
|
||||||
def stir_time(self) -> float:
|
|
||||||
return self._stir_time
|
|
||||||
|
|
||||||
@property
|
|
||||||
def stir_speed(self) -> float:
|
|
||||||
return self._stir_speed
|
|
||||||
|
|
||||||
@property
|
|
||||||
def time_spent(self) -> float:
|
|
||||||
if self._running:
|
|
||||||
self._time_spent = datetime.now() - self._start_time
|
|
||||||
return self._time_spent.total_seconds()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def time_remaining(self) -> float:
|
|
||||||
if self._running:
|
|
||||||
elapsed = (datetime.now() - self._start_time).total_seconds()
|
|
||||||
total_time = (self._stir_time + self._settling_time + 10) * self._repeats
|
|
||||||
remain = max(0, total_time - elapsed)
|
|
||||||
self._time_remaining = timedelta(seconds=remain)
|
|
||||||
return self._time_remaining.total_seconds()
|
|
||||||
|
|
||||||
def separate(self, purpose: str, product_phase: str, from_vessel: str,
|
|
||||||
separation_vessel: str, to_vessel: str, waste_phase_to_vessel: str = "",
|
|
||||||
solvent: str = "", solvent_volume: float = 0.0, through: str = "",
|
|
||||||
repeats: int = 1, stir_time: float = 0.0, stir_speed: float = 0.0,
|
|
||||||
settling_time: float = 60.0) -> dict:
|
|
||||||
"""
|
|
||||||
执行分离操作
|
|
||||||
"""
|
|
||||||
with self._thread_lock:
|
|
||||||
# 检查是否已经在运行
|
|
||||||
if self._running:
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"status": "Error: Operation already in progress"
|
|
||||||
}
|
|
||||||
# 必填参数验证
|
|
||||||
if not all([from_vessel, separation_vessel, to_vessel]):
|
|
||||||
self._status = "Error: Missing required vessel parameters"
|
|
||||||
return {"success": False}
|
|
||||||
# 验证参数
|
|
||||||
if purpose not in ["wash", "extract"]:
|
|
||||||
self._status = "Error: Invalid purpose"
|
|
||||||
return {"success": False}
|
|
||||||
|
|
||||||
if product_phase not in ["top", "bottom"]:
|
|
||||||
self._status = "Error: Invalid product phase"
|
|
||||||
return {"success": False}
|
|
||||||
# 数值参数验证
|
|
||||||
try:
|
|
||||||
solvent_volume = float(solvent_volume)
|
|
||||||
repeats = int(repeats)
|
|
||||||
stir_time = float(stir_time)
|
|
||||||
stir_speed = float(stir_speed)
|
|
||||||
settling_time = float(settling_time)
|
|
||||||
except ValueError:
|
|
||||||
self._status = "Error: Invalid numeric parameters"
|
|
||||||
return {"success": False}
|
|
||||||
|
|
||||||
# 设置参数
|
|
||||||
self._purpose = purpose
|
|
||||||
self._product_phase = product_phase
|
|
||||||
self._from_vessel = from_vessel
|
|
||||||
self._separation_vessel = separation_vessel
|
|
||||||
self._to_vessel = to_vessel
|
|
||||||
self._waste_phase_to_vessel = waste_phase_to_vessel
|
|
||||||
self._solvent = solvent
|
|
||||||
self._solvent_volume = float(solvent_volume)
|
|
||||||
self._through = through
|
|
||||||
self._repeats = int(repeats)
|
|
||||||
self._stir_time = float(stir_time)
|
|
||||||
self._stir_speed = float(stir_speed)
|
|
||||||
self._settling_time = float(settling_time)
|
|
||||||
|
|
||||||
# 重置计时器
|
|
||||||
self._start_time = datetime.now()
|
|
||||||
self._time_spent = timedelta()
|
|
||||||
total_time = (self._stir_time + self._settling_time + 10) * self._repeats
|
|
||||||
self._time_remaining = timedelta(seconds=total_time)
|
|
||||||
|
|
||||||
# 启动分离操作
|
|
||||||
self._status = "Starting Separation"
|
|
||||||
self._running = True
|
|
||||||
|
|
||||||
# 在锁内创建和启动线程
|
|
||||||
self._operation_thread = threading.Thread(target=self._operation_loop)
|
|
||||||
self._operation_thread.daemon = True
|
|
||||||
self._operation_thread.start()
|
|
||||||
|
|
||||||
# 等待确认操作已经开始
|
|
||||||
time.sleep(0.1) # 短暂等待确保操作线程已启动
|
|
||||||
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"status": self._status,
|
|
||||||
"current_device": self._current_device,
|
|
||||||
"time_spent": self._time_spent.total_seconds(),
|
|
||||||
"time_remaining": self._time_remaining.total_seconds()
|
|
||||||
}
|
|
||||||
|
|
||||||
def shake(self, shake_time: float) -> str:
|
|
||||||
"""
|
|
||||||
模拟 shake(搅拌)操作:
|
|
||||||
- 进入 "Shaking" 状态,倒计时 shake_time 秒
|
|
||||||
- shake 结束后,进入 "Settling" 状态,静置时间固定为 5 秒
|
|
||||||
- 最后恢复为 Idle
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
shake_time = float(shake_time)
|
|
||||||
except ValueError:
|
|
||||||
self._status = "Error: Invalid shake time"
|
|
||||||
return "Error"
|
|
||||||
|
|
||||||
with self._thread_lock:
|
|
||||||
self._status = "Shaking"
|
|
||||||
self._settling_time = 0.0
|
|
||||||
self._shake_time = shake_time
|
|
||||||
self._shake_status = "Shaking"
|
|
||||||
|
|
||||||
def _run_shake():
|
|
||||||
remaining = shake_time
|
|
||||||
while remaining > 0:
|
|
||||||
time.sleep(1)
|
|
||||||
remaining -= 1
|
|
||||||
with self._thread_lock:
|
|
||||||
self._shake_time = remaining
|
|
||||||
with self._thread_lock:
|
|
||||||
self._status = "Settling"
|
|
||||||
self._settling_time = 60.0 # 固定静置时间为60秒
|
|
||||||
self._shake_status = "Settling"
|
|
||||||
while True:
|
|
||||||
with self._thread_lock:
|
|
||||||
if self._settling_time <= 0:
|
|
||||||
self._status = "Idle"
|
|
||||||
self._shake_status = "Idle"
|
|
||||||
break
|
|
||||||
time.sleep(1)
|
|
||||||
with self._thread_lock:
|
|
||||||
self._settling_time = max(0.0, self._settling_time - 1)
|
|
||||||
|
|
||||||
self._operation_thread = threading.Thread(target=_run_shake)
|
|
||||||
self._operation_thread.daemon = True
|
|
||||||
self._operation_thread.start()
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
def set_valve(self, command: str) -> str:
|
|
||||||
"""
|
|
||||||
阀门控制命令:传入 "open" 或 "close"
|
|
||||||
"""
|
|
||||||
|
|
||||||
command = command.lower()
|
|
||||||
if command == "open":
|
|
||||||
self._valve_state = "Open"
|
|
||||||
self._status = "Valve Opened"
|
|
||||||
elif command == "close":
|
|
||||||
self._valve_state = "Closed"
|
|
||||||
self._status = "Valve Closed"
|
|
||||||
else:
|
|
||||||
self._status = "Error: Invalid valve command"
|
|
||||||
return "Error"
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
def _operation_loop(self):
|
|
||||||
"""分离操作主循环"""
|
|
||||||
try:
|
|
||||||
current_repeat = 1
|
|
||||||
|
|
||||||
# 立即更新状态,确保不会停留在Starting Separation
|
|
||||||
with self._thread_lock:
|
|
||||||
self._status = f"Separation Cycle {current_repeat}/{self._repeats}"
|
|
||||||
|
|
||||||
while self._running and current_repeat <= self._repeats:
|
|
||||||
# 第一步:搅拌
|
|
||||||
if self._stir_time > 0:
|
|
||||||
with self._thread_lock:
|
|
||||||
self._status = f"Stirring (Repeat {current_repeat}/{self._repeats})"
|
|
||||||
remaining_stir = self._stir_time
|
|
||||||
while remaining_stir > 0 and self._running:
|
|
||||||
time.sleep(1)
|
|
||||||
remaining_stir -= 1
|
|
||||||
|
|
||||||
# 第二步:静置
|
|
||||||
if self._settling_time > 0:
|
|
||||||
with self._thread_lock:
|
|
||||||
self._status = f"Settling (Repeat {current_repeat}/{self._repeats})"
|
|
||||||
remaining_settle = self._settling_time
|
|
||||||
while remaining_settle > 0 and self._running:
|
|
||||||
time.sleep(1)
|
|
||||||
remaining_settle -= 1
|
|
||||||
|
|
||||||
# 第三步:打开阀门排出
|
|
||||||
with self._thread_lock:
|
|
||||||
self._valve_state = "Open"
|
|
||||||
self._status = f"Draining (Repeat {current_repeat}/{self._repeats})"
|
|
||||||
|
|
||||||
# 模拟排出时间(5秒)
|
|
||||||
time.sleep(10)
|
|
||||||
|
|
||||||
# 关闭阀门
|
|
||||||
with self._thread_lock:
|
|
||||||
self._valve_state = "Closed"
|
|
||||||
|
|
||||||
# 检查是否继续下一次重复
|
|
||||||
if current_repeat < self._repeats:
|
|
||||||
current_repeat += 1
|
|
||||||
else:
|
|
||||||
with self._thread_lock:
|
|
||||||
self._status = "Separation Complete"
|
|
||||||
break
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
with self._thread_lock:
|
|
||||||
self._status = f"Error in separation: {str(e)}"
|
|
||||||
finally:
|
|
||||||
with self._thread_lock:
|
|
||||||
self._running = False
|
|
||||||
self._valve_state = "Closed"
|
|
||||||
if self._status == "Starting Separation":
|
|
||||||
self._status = "Error: Operation failed to start"
|
|
||||||
elif self._status != "Separation Complete":
|
|
||||||
self._status = "Stopped"
|
|
||||||
|
|
||||||
def stop_operations(self) -> str:
|
|
||||||
"""停止任何正在执行的操作"""
|
|
||||||
with self._thread_lock:
|
|
||||||
self._running = False
|
|
||||||
if self._operation_thread and self._operation_thread.is_alive():
|
|
||||||
self._operation_thread.join(timeout=1.0)
|
|
||||||
self._operation_thread = None
|
|
||||||
self._settling_time = 0.0
|
|
||||||
self._status = "Idle"
|
|
||||||
self._shake_status = "Idle"
|
|
||||||
self._shake_time = 0.0
|
|
||||||
self._time_remaining = timedelta()
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
def get_status_info(self) -> dict:
|
|
||||||
"""获取当前设备状态信息"""
|
|
||||||
with self._thread_lock:
|
|
||||||
current_time = datetime.now()
|
|
||||||
if self._start_time:
|
|
||||||
self._time_spent = current_time - self._start_time
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": self._status,
|
|
||||||
"valve_state": self._valve_state,
|
|
||||||
"settling_time": self._settling_time,
|
|
||||||
"shake_time": self._shake_time,
|
|
||||||
"shake_status": self._shake_status,
|
|
||||||
"current_device": self._current_device,
|
|
||||||
"purpose": self._purpose,
|
|
||||||
"product_phase": self._product_phase,
|
|
||||||
"from_vessel": self._from_vessel,
|
|
||||||
"separation_vessel": self._separation_vessel,
|
|
||||||
"to_vessel": self._to_vessel,
|
|
||||||
"waste_phase_to_vessel": self._waste_phase_to_vessel,
|
|
||||||
"solvent": self._solvent,
|
|
||||||
"solvent_volume": self._solvent_volume,
|
|
||||||
"through": self._through,
|
|
||||||
"repeats": self._repeats,
|
|
||||||
"stir_time": self._stir_time,
|
|
||||||
"stir_speed": self._stir_speed,
|
|
||||||
"time_spent": self._time_spent.total_seconds(),
|
|
||||||
"time_remaining": self._time_remaining.total_seconds()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# 主函数用于测试
|
|
||||||
if __name__ == "__main__":
|
|
||||||
separator = MockSeparator()
|
|
||||||
|
|
||||||
print("启动简单版分离器测试...")
|
|
||||||
print("初始状态:", separator.get_status_info())
|
|
||||||
|
|
||||||
# 触发 shake 操作,模拟 10 秒的搅拌
|
|
||||||
print("执行 shake 操作...")
|
|
||||||
print(separator.shake(10.0))
|
|
||||||
|
|
||||||
# 循环显示状态变化
|
|
||||||
for i in range(20):
|
|
||||||
time.sleep(1)
|
|
||||||
info = separator.get_status_info()
|
|
||||||
print(
|
|
||||||
f"第{i+1}秒: 状态={info['status']}, 静置时间={info['settling_time']:.1f}秒, "
|
|
||||||
f"阀门状态={info['valve_state']}, shake_time={info['shake_time']:.1f}, "
|
|
||||||
f"shake_status={info['shake_status']}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 模拟打开阀门
|
|
||||||
print("打开阀门...", separator.set_valve("open"))
|
|
||||||
print("最终状态:", separator.get_status_info())
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import time
|
|
||||||
|
|
||||||
|
|
||||||
class MockSolenoidValve:
|
|
||||||
"""
|
|
||||||
模拟电磁阀设备类 - 简化版本
|
|
||||||
|
|
||||||
这个类提供了电磁阀的基本功能:开启、关闭和状态查询
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, port: str = "MOCK"):
|
|
||||||
"""
|
|
||||||
初始化MockSolenoidValve实例
|
|
||||||
|
|
||||||
Args:
|
|
||||||
port (str): 设备端口,默认为"MOCK"表示模拟设备
|
|
||||||
"""
|
|
||||||
self.port = port
|
|
||||||
self._status: str = "Idle"
|
|
||||||
self._valve_status: str = "Closed" # 阀门位置:Open, Closed
|
|
||||||
|
|
||||||
@property
|
|
||||||
def status(self) -> str:
|
|
||||||
"""设备状态 - 会被自动识别的设备属性"""
|
|
||||||
return self._status
|
|
||||||
|
|
||||||
@property
|
|
||||||
def valve_status(self) -> str:
|
|
||||||
"""阀门状态"""
|
|
||||||
return self._valve_status
|
|
||||||
|
|
||||||
def set_valve_status(self, status: str) -> str:
|
|
||||||
"""
|
|
||||||
设置阀门位置
|
|
||||||
|
|
||||||
Args:
|
|
||||||
position (str): 阀门位置,可选值:"Open", "Closed"
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 操作结果状态 ("Success", "Error")
|
|
||||||
"""
|
|
||||||
if status not in ["Open", "Closed"]:
|
|
||||||
self._status = "Error: Invalid position"
|
|
||||||
return "Error"
|
|
||||||
|
|
||||||
self._status = "Moving"
|
|
||||||
time.sleep(1) # 模拟阀门动作时间
|
|
||||||
|
|
||||||
self._valve_status = status
|
|
||||||
self._status = "Idle"
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
def open_valve(self) -> str:
|
|
||||||
"""打开阀门"""
|
|
||||||
return self.set_valve_status("Open")
|
|
||||||
|
|
||||||
def close_valve(self) -> str:
|
|
||||||
"""关闭阀门"""
|
|
||||||
return self.set_valve_status("Closed")
|
|
||||||
|
|
||||||
def get_valve_status(self) -> str:
|
|
||||||
"""获取阀门位置"""
|
|
||||||
return self._valve_status
|
|
||||||
|
|
||||||
def is_open(self) -> bool:
|
|
||||||
"""检查阀门是否打开"""
|
|
||||||
return self._valve_status == "Open"
|
|
||||||
|
|
||||||
def is_closed(self) -> bool:
|
|
||||||
"""检查阀门是否关闭"""
|
|
||||||
return self._valve_status == "Closed"
|
|
||||||
|
|
||||||
|
|
||||||
# 用于测试的主函数
|
|
||||||
if __name__ == "__main__":
|
|
||||||
valve = MockSolenoidValve()
|
|
||||||
|
|
||||||
print("启动电磁阀测试...")
|
|
||||||
print(f"初始状态: 位置={valve.valve_status}, 状态={valve.status}")
|
|
||||||
|
|
||||||
# 测试开启阀门
|
|
||||||
valve.open_valve()
|
|
||||||
print(f"开启后: 位置={valve.valve_status}, 状态={valve.status}")
|
|
||||||
|
|
||||||
# 测试关闭阀门
|
|
||||||
valve.close_valve()
|
|
||||||
print(f"关闭后: 位置={valve.valve_status}, 状态={valve.status}")
|
|
||||||
|
|
||||||
print("测试完成")
|
|
||||||
@@ -1,307 +0,0 @@
|
|||||||
import time
|
|
||||||
import threading
|
|
||||||
|
|
||||||
|
|
||||||
class MockStirrer:
|
|
||||||
def __init__(self, port: str = "MOCK"):
|
|
||||||
self.port = port
|
|
||||||
|
|
||||||
# 设备基本状态属性
|
|
||||||
self._status: str = "Idle" # 设备状态:Idle, Running, Error, Stopped
|
|
||||||
|
|
||||||
# 搅拌相关属性
|
|
||||||
self._stir_speed: float = 0.0 # 当前搅拌速度 (rpm)
|
|
||||||
self._target_stir_speed: float = 0.0 # 目标搅拌速度 (rpm)
|
|
||||||
self._max_stir_speed: float = 2000.0 # 最大搅拌速度 (rpm)
|
|
||||||
self._stir_state: str = "Stopped" # 搅拌状态:Running, Stopped
|
|
||||||
|
|
||||||
# 温度相关属性
|
|
||||||
self._temperature: float = 25.0 # 当前温度 (°C)
|
|
||||||
self._target_temperature: float = 25.0 # 目标温度 (°C)
|
|
||||||
self._max_temperature: float = 300.0 # 最大温度 (°C)
|
|
||||||
self._heating_state: str = "Off" # 加热状态:On, Off
|
|
||||||
self._heating_power: float = 0.0 # 加热功率百分比 0-100
|
|
||||||
|
|
||||||
# 运行控制线程
|
|
||||||
self._operation_thread = None
|
|
||||||
self._running = False
|
|
||||||
self._thread_lock = threading.Lock()
|
|
||||||
|
|
||||||
# ==================== 状态属性 ====================
|
|
||||||
# 这些属性会被Uni-Lab系统自动识别并定时对外广播
|
|
||||||
|
|
||||||
@property
|
|
||||||
def status(self) -> str:
|
|
||||||
return self._status
|
|
||||||
|
|
||||||
@property
|
|
||||||
def stir_speed(self) -> float:
|
|
||||||
return self._stir_speed
|
|
||||||
|
|
||||||
@property
|
|
||||||
def target_stir_speed(self) -> float:
|
|
||||||
return self._target_stir_speed
|
|
||||||
|
|
||||||
@property
|
|
||||||
def stir_state(self) -> str:
|
|
||||||
return self._stir_state
|
|
||||||
|
|
||||||
@property
|
|
||||||
def temperature(self) -> float:
|
|
||||||
"""
|
|
||||||
当前温度
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: 当前温度 (°C)
|
|
||||||
"""
|
|
||||||
return self._temperature
|
|
||||||
|
|
||||||
@property
|
|
||||||
def target_temperature(self) -> float:
|
|
||||||
"""
|
|
||||||
目标温度
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: 目标温度 (°C)
|
|
||||||
"""
|
|
||||||
return self._target_temperature
|
|
||||||
|
|
||||||
@property
|
|
||||||
def heating_state(self) -> str:
|
|
||||||
return self._heating_state
|
|
||||||
|
|
||||||
@property
|
|
||||||
def heating_power(self) -> float:
|
|
||||||
return self._heating_power
|
|
||||||
|
|
||||||
@property
|
|
||||||
def max_stir_speed(self) -> float:
|
|
||||||
return self._max_stir_speed
|
|
||||||
|
|
||||||
@property
|
|
||||||
def max_temperature(self) -> float:
|
|
||||||
return self._max_temperature
|
|
||||||
|
|
||||||
# ==================== 设备控制方法 ====================
|
|
||||||
# 这些方法需要在注册表中添加,会作为ActionServer接受控制指令
|
|
||||||
|
|
||||||
def set_stir_speed(self, speed: float) -> str:
|
|
||||||
|
|
||||||
speed = float(speed) # 确保传入的速度是浮点数
|
|
||||||
|
|
||||||
if speed < 0 or speed > self._max_stir_speed:
|
|
||||||
self._status = f"Error: Speed out of range (0-{self._max_stir_speed})"
|
|
||||||
return "Error"
|
|
||||||
|
|
||||||
self._target_stir_speed = speed
|
|
||||||
self._status = "Setting Stir Speed"
|
|
||||||
|
|
||||||
# 如果设置了非零速度,启动搅拌
|
|
||||||
if speed > 0:
|
|
||||||
self._stir_state = "Running"
|
|
||||||
else:
|
|
||||||
self._stir_state = "Stopped"
|
|
||||||
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
def set_temperature(self, temperature: float) -> str:
|
|
||||||
temperature = float(temperature) # 确保传入的温度是浮点数
|
|
||||||
|
|
||||||
if temperature < 0 or temperature > self._max_temperature:
|
|
||||||
self._status = f"Error: Temperature out of range (0-{self._max_temperature})"
|
|
||||||
return "Error"
|
|
||||||
|
|
||||||
self._target_temperature = temperature
|
|
||||||
self._status = "Setting Temperature"
|
|
||||||
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
def start_stirring(self) -> str:
|
|
||||||
|
|
||||||
if self._target_stir_speed <= 0:
|
|
||||||
self._status = "Error: No target speed set"
|
|
||||||
return "Error"
|
|
||||||
|
|
||||||
self._stir_state = "Running"
|
|
||||||
self._status = "Stirring Started"
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
def stop_stirring(self) -> str:
|
|
||||||
self._stir_state = "Stopped"
|
|
||||||
self._target_stir_speed = 0.0
|
|
||||||
self._status = "Stirring Stopped"
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
def heating_control(self, heating_state: str = "On") -> str:
|
|
||||||
|
|
||||||
if heating_state not in ["On", "Off"]:
|
|
||||||
self._status = "Error: Invalid heating state"
|
|
||||||
return "Error"
|
|
||||||
|
|
||||||
self._heating_state = heating_state
|
|
||||||
|
|
||||||
if heating_state == "On":
|
|
||||||
self._status = "Heating On"
|
|
||||||
else:
|
|
||||||
self._status = "Heating Off"
|
|
||||||
self._heating_power = 0.0
|
|
||||||
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
def stop_all_operations(self) -> str:
|
|
||||||
self._stir_state = "Stopped"
|
|
||||||
self._heating_state = "Off"
|
|
||||||
self._stop_operation()
|
|
||||||
self._stir_speed = 0.0
|
|
||||||
self._target_stir_speed = 0.0
|
|
||||||
self._heating_power = 0.0
|
|
||||||
self._status = "All operations stopped"
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
def emergency_stop(self) -> str:
|
|
||||||
"""
|
|
||||||
紧急停止
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 操作结果状态 ("Success", "Error")
|
|
||||||
"""
|
|
||||||
self._status = "Emergency Stop"
|
|
||||||
self.stop_all_operations()
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
# ==================== 内部控制方法 ====================
|
|
||||||
|
|
||||||
def _start_operation(self):
|
|
||||||
with self._thread_lock:
|
|
||||||
if not self._running:
|
|
||||||
self._running = True
|
|
||||||
self._operation_thread = threading.Thread(target=self._operation_loop)
|
|
||||||
self._operation_thread.daemon = True
|
|
||||||
self._operation_thread.start()
|
|
||||||
|
|
||||||
def _stop_operation(self):
|
|
||||||
"""
|
|
||||||
停止操作线程
|
|
||||||
|
|
||||||
安全地停止后台运行线程并等待其完成。
|
|
||||||
"""
|
|
||||||
with self._thread_lock:
|
|
||||||
self._running = False
|
|
||||||
if self._operation_thread and self._operation_thread.is_alive():
|
|
||||||
self._operation_thread.join(timeout=2.0)
|
|
||||||
|
|
||||||
def _operation_loop(self):
|
|
||||||
while self._running:
|
|
||||||
try:
|
|
||||||
# 处理搅拌速度控制
|
|
||||||
if self._stir_state == "Running":
|
|
||||||
speed_diff = self._target_stir_speed - self._stir_speed
|
|
||||||
|
|
||||||
if abs(speed_diff) < 1.0: # 速度接近目标值
|
|
||||||
self._stir_speed = self._target_stir_speed
|
|
||||||
if self._stir_speed > 0:
|
|
||||||
self._status = "Stirring at Target Speed"
|
|
||||||
else:
|
|
||||||
# 模拟速度调节,每秒调整10%的差值
|
|
||||||
adjustment = speed_diff * 0.1
|
|
||||||
self._stir_speed += adjustment
|
|
||||||
self._status = "Adjusting Stir Speed"
|
|
||||||
|
|
||||||
# 确保速度在合理范围内
|
|
||||||
self._stir_speed = max(0.0, min(self._max_stir_speed, self._stir_speed))
|
|
||||||
else:
|
|
||||||
# 搅拌停止时,速度逐渐降为0
|
|
||||||
if self._stir_speed > 0:
|
|
||||||
self._stir_speed = max(0.0, self._stir_speed - 50.0) # 每秒减少50rpm
|
|
||||||
|
|
||||||
# 处理温度控制
|
|
||||||
if self._heating_state == "On":
|
|
||||||
temp_diff = self._target_temperature - self._temperature
|
|
||||||
|
|
||||||
if abs(temp_diff) < 0.5: # 温度接近目标值
|
|
||||||
self._heating_power = 20.0 # 维持温度的最小功率
|
|
||||||
elif temp_diff > 0: # 需要加热
|
|
||||||
# 根据温差调整加热功率
|
|
||||||
if temp_diff > 50:
|
|
||||||
self._heating_power = 100.0
|
|
||||||
elif temp_diff > 20:
|
|
||||||
self._heating_power = 80.0
|
|
||||||
elif temp_diff > 10:
|
|
||||||
self._heating_power = 60.0
|
|
||||||
else:
|
|
||||||
self._heating_power = 40.0
|
|
||||||
|
|
||||||
# 模拟加热过程
|
|
||||||
heating_rate = self._heating_power / 100.0 * 1.5 # 最大每秒升温1.5度
|
|
||||||
self._temperature += heating_rate
|
|
||||||
else: # 目标温度低于当前温度
|
|
||||||
self._heating_power = 0.0
|
|
||||||
# 自然冷却
|
|
||||||
self._temperature -= 0.1
|
|
||||||
else:
|
|
||||||
self._heating_power = 0.0
|
|
||||||
# 自然冷却到室温
|
|
||||||
if self._temperature > 25.0:
|
|
||||||
self._temperature -= 0.2
|
|
||||||
|
|
||||||
# 限制温度范围
|
|
||||||
self._temperature = max(20.0, min(self._max_temperature, self._temperature))
|
|
||||||
|
|
||||||
# 更新整体状态
|
|
||||||
if self._stir_state == "Running" and self._heating_state == "On":
|
|
||||||
self._status = "Stirring and Heating"
|
|
||||||
elif self._stir_state == "Running":
|
|
||||||
self._status = "Stirring Only"
|
|
||||||
elif self._heating_state == "On":
|
|
||||||
self._status = "Heating Only"
|
|
||||||
else:
|
|
||||||
self._status = "Idle"
|
|
||||||
|
|
||||||
# 等待1秒后继续下一次循环
|
|
||||||
time.sleep(1.0)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._status = f"Error in operation: {str(e)}"
|
|
||||||
break
|
|
||||||
|
|
||||||
# 循环结束时的清理工作
|
|
||||||
self._status = "Idle"
|
|
||||||
|
|
||||||
def get_status_info(self) -> dict:
|
|
||||||
return {
|
|
||||||
"status": self._status,
|
|
||||||
"stir_speed": self._stir_speed,
|
|
||||||
"target_stir_speed": self._target_stir_speed,
|
|
||||||
"stir_state": self._stir_state,
|
|
||||||
"temperature": self._temperature,
|
|
||||||
"target_temperature": self._target_temperature,
|
|
||||||
"heating_state": self._heating_state,
|
|
||||||
"heating_power": self._heating_power,
|
|
||||||
"max_stir_speed": self._max_stir_speed,
|
|
||||||
"max_temperature": self._max_temperature,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# 用于测试的主函数
|
|
||||||
if __name__ == "__main__":
|
|
||||||
stirrer = MockStirrer()
|
|
||||||
|
|
||||||
# 测试基本功能
|
|
||||||
print("启动搅拌器测试...")
|
|
||||||
print(f"初始状态: {stirrer.get_status_info()}")
|
|
||||||
|
|
||||||
# 设置搅拌速度和温度
|
|
||||||
stirrer.set_stir_speed(800.0)
|
|
||||||
stirrer.set_temperature(60.0)
|
|
||||||
stirrer.heating_control("On")
|
|
||||||
|
|
||||||
# 模拟运行15秒
|
|
||||||
for i in range(15):
|
|
||||||
time.sleep(1)
|
|
||||||
print(
|
|
||||||
f"第{i+1}秒: 速度={stirrer.stir_speed:.0f}rpm, 温度={stirrer.temperature:.1f}°C, "
|
|
||||||
f"功率={stirrer.heating_power:.1f}%, 状态={stirrer.status}"
|
|
||||||
)
|
|
||||||
|
|
||||||
stirrer.emergency_stop()
|
|
||||||
print("测试完成")
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
import time
|
|
||||||
import threading
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
class MockStirrer_new:
|
|
||||||
def __init__(self, port: str = "MOCK"):
|
|
||||||
self.port = port
|
|
||||||
|
|
||||||
# 基本状态属性
|
|
||||||
self._status: str = "Idle"
|
|
||||||
self._vessel: str = ""
|
|
||||||
self._purpose: str = ""
|
|
||||||
|
|
||||||
# 搅拌相关属性
|
|
||||||
self._stir_speed: float = 0.0
|
|
||||||
self._target_stir_speed: float = 0.0
|
|
||||||
self._max_stir_speed: float = 2000.0
|
|
||||||
self._stir_state: str = "Stopped"
|
|
||||||
|
|
||||||
# 计时相关
|
|
||||||
self._stir_time: float = 0.0
|
|
||||||
self._settling_time: float = 0.0
|
|
||||||
self._start_time = datetime.now()
|
|
||||||
self._time_remaining = timedelta()
|
|
||||||
|
|
||||||
# 运行控制
|
|
||||||
self._operation_thread = None
|
|
||||||
self._running = False
|
|
||||||
self._thread_lock = threading.Lock()
|
|
||||||
|
|
||||||
# 创建操作线程
|
|
||||||
self._operation_thread = threading.Thread(target=self._operation_loop)
|
|
||||||
self._operation_thread.daemon = True
|
|
||||||
self._operation_thread.start()
|
|
||||||
|
|
||||||
# ==================== 状态属性 ====================
|
|
||||||
@property
|
|
||||||
def status(self) -> str:
|
|
||||||
return self._status
|
|
||||||
|
|
||||||
@property
|
|
||||||
def stir_speed(self) -> float:
|
|
||||||
return self._stir_speed
|
|
||||||
|
|
||||||
@property
|
|
||||||
def target_stir_speed(self) -> float:
|
|
||||||
return self._target_stir_speed
|
|
||||||
|
|
||||||
@property
|
|
||||||
def stir_state(self) -> str:
|
|
||||||
return self._stir_state
|
|
||||||
|
|
||||||
@property
|
|
||||||
def vessel(self) -> str:
|
|
||||||
return self._vessel
|
|
||||||
|
|
||||||
@property
|
|
||||||
def purpose(self) -> str:
|
|
||||||
return self._purpose
|
|
||||||
|
|
||||||
@property
|
|
||||||
def stir_time(self) -> float:
|
|
||||||
return self._stir_time
|
|
||||||
|
|
||||||
@property
|
|
||||||
def settling_time(self) -> float:
|
|
||||||
return self._settling_time
|
|
||||||
|
|
||||||
@property
|
|
||||||
def max_stir_speed(self) -> float:
|
|
||||||
return self._max_stir_speed
|
|
||||||
|
|
||||||
@property
|
|
||||||
def progress(self) -> float:
|
|
||||||
"""返回当前操作的进度(0-100)"""
|
|
||||||
if not self._running:
|
|
||||||
return 0.0
|
|
||||||
elapsed = (datetime.now() - self._start_time).total_seconds()
|
|
||||||
total_time = self._stir_time + self._settling_time
|
|
||||||
if total_time <= 0:
|
|
||||||
return 100.0
|
|
||||||
return min(100.0, (elapsed / total_time) * 100)
|
|
||||||
|
|
||||||
# ==================== Action Server 方法 ====================
|
|
||||||
def start_stir(self, vessel: str, stir_speed: float = 0.0, purpose: str = "") -> dict:
|
|
||||||
"""
|
|
||||||
StartStir.action 对应的方法
|
|
||||||
"""
|
|
||||||
with self._thread_lock:
|
|
||||||
if self._running:
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"message": "Operation already in progress"
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 重置所有参数
|
|
||||||
self._vessel = vessel
|
|
||||||
self._purpose = purpose
|
|
||||||
self._stir_time = 0.0 # 连续搅拌模式下不设置搅拌时间
|
|
||||||
self._settling_time = 0.0
|
|
||||||
self._start_time = datetime.now() # 重置开始时间
|
|
||||||
|
|
||||||
if stir_speed > 0:
|
|
||||||
self._target_stir_speed = min(stir_speed, self._max_stir_speed)
|
|
||||||
|
|
||||||
self._stir_state = "Running"
|
|
||||||
self._status = "Stirring Started"
|
|
||||||
self._running = True
|
|
||||||
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"message": "Stirring started successfully"
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"message": f"Error: {str(e)}"
|
|
||||||
}
|
|
||||||
|
|
||||||
def stir(self, stir_time: float, stir_speed: float, settling_time: float) -> dict:
|
|
||||||
"""
|
|
||||||
Stir.action 对应的方法
|
|
||||||
"""
|
|
||||||
with self._thread_lock:
|
|
||||||
try:
|
|
||||||
# 如果已经在运行,先停止当前操作
|
|
||||||
if self._running:
|
|
||||||
self._running = False
|
|
||||||
self._stir_state = "Stopped"
|
|
||||||
self._target_stir_speed = 0.0
|
|
||||||
time.sleep(0.1) # 给一个短暂的停止时间
|
|
||||||
|
|
||||||
|
|
||||||
# 重置所有参数
|
|
||||||
self._stir_time = float(stir_time)
|
|
||||||
self._settling_time = float(settling_time)
|
|
||||||
self._target_stir_speed = min(float(stir_speed), self._max_stir_speed)
|
|
||||||
self._start_time = datetime.now() # 重置开始时间
|
|
||||||
self._stir_state = "Running"
|
|
||||||
self._status = "Stirring"
|
|
||||||
self._running = True
|
|
||||||
|
|
||||||
return {"success": True}
|
|
||||||
|
|
||||||
except ValueError:
|
|
||||||
self._status = "Error: Invalid parameters"
|
|
||||||
return {"success": False}
|
|
||||||
|
|
||||||
def stop_stir(self, vessel: str) -> dict:
|
|
||||||
"""
|
|
||||||
StopStir.action 对应的方法
|
|
||||||
"""
|
|
||||||
with self._thread_lock:
|
|
||||||
if vessel != self._vessel:
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"message": "Vessel mismatch"
|
|
||||||
}
|
|
||||||
|
|
||||||
self._running = False
|
|
||||||
self._stir_state = "Stopped"
|
|
||||||
self._target_stir_speed = 0.0
|
|
||||||
self._status = "Stirring Stopped"
|
|
||||||
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"message": "Stirring stopped successfully"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ==================== 内部控制方法 ====================
|
|
||||||
|
|
||||||
def _operation_loop(self):
|
|
||||||
"""操作主循环"""
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
current_time = datetime.now()
|
|
||||||
|
|
||||||
with self._thread_lock: # 添加锁保护
|
|
||||||
if self._stir_state == "Running":
|
|
||||||
# 实际搅拌逻辑
|
|
||||||
speed_diff = self._target_stir_speed - self._stir_speed
|
|
||||||
if abs(speed_diff) > 0.1:
|
|
||||||
adjustment = speed_diff * 0.1
|
|
||||||
self._stir_speed += adjustment
|
|
||||||
else:
|
|
||||||
self._stir_speed = self._target_stir_speed
|
|
||||||
|
|
||||||
# 更新进度
|
|
||||||
if self._running:
|
|
||||||
if self._stir_time > 0: # 定时搅拌模式
|
|
||||||
elapsed = (current_time - self._start_time).total_seconds()
|
|
||||||
if elapsed >= self._stir_time + self._settling_time:
|
|
||||||
self._running = False
|
|
||||||
self._stir_state = "Stopped"
|
|
||||||
self._target_stir_speed = 0.0
|
|
||||||
self._stir_speed = 0.0
|
|
||||||
self._status = "Stirring Complete"
|
|
||||||
elif elapsed >= self._stir_time:
|
|
||||||
self._status = "Settling"
|
|
||||||
else: # 连续搅拌模式
|
|
||||||
self._status = "Stirring"
|
|
||||||
else:
|
|
||||||
# 停止状态下慢慢降低速度
|
|
||||||
if self._stir_speed > 0:
|
|
||||||
self._stir_speed = max(0, self._stir_speed - 20.0)
|
|
||||||
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error in operation loop: {str(e)}") # 添加错误输出
|
|
||||||
self._status = f"Error: {str(e)}"
|
|
||||||
time.sleep(1.0) # 错误发生时等待较长时间
|
|
||||||
|
|
||||||
def get_status_info(self) -> dict:
|
|
||||||
"""获取设备状态信息"""
|
|
||||||
return {
|
|
||||||
"status": self._status,
|
|
||||||
"vessel": self._vessel,
|
|
||||||
"purpose": self._purpose,
|
|
||||||
"stir_speed": self._stir_speed,
|
|
||||||
"target_stir_speed": self._target_stir_speed,
|
|
||||||
"stir_state": self._stir_state,
|
|
||||||
"stir_time": self._stir_time, # 添加
|
|
||||||
"settling_time": self._settling_time, # 添加
|
|
||||||
"progress": self.progress,
|
|
||||||
"max_stir_speed": self._max_stir_speed
|
|
||||||
}
|
|
||||||
@@ -1,410 +0,0 @@
|
|||||||
import time
|
|
||||||
import threading
|
|
||||||
|
|
||||||
|
|
||||||
class MockVacuum:
|
|
||||||
"""
|
|
||||||
模拟真空泵设备类
|
|
||||||
|
|
||||||
这个类模拟了一个实验室真空泵的行为,包括真空度控制、
|
|
||||||
压力监测、运行状态管理等功能。参考了现有的 VacuumPumpMock 实现。
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, port: str = "MOCK"):
|
|
||||||
"""
|
|
||||||
初始化MockVacuum实例
|
|
||||||
|
|
||||||
Args:
|
|
||||||
port (str): 设备端口,默认为"MOCK"表示模拟设备
|
|
||||||
"""
|
|
||||||
self.port = port
|
|
||||||
|
|
||||||
# 设备基本状态属性
|
|
||||||
self._status: str = "Idle" # 设备状态:Idle, Running, Error, Stopped
|
|
||||||
self._power_state: str = "Off" # 电源状态:On, Off
|
|
||||||
self._pump_state: str = "Stopped" # 泵运行状态:Running, Stopped, Paused
|
|
||||||
|
|
||||||
# 真空相关属性
|
|
||||||
self._vacuum_level: float = 1013.25 # 当前真空度 (mbar) - 大气压开始
|
|
||||||
self._target_vacuum: float = 50.0 # 目标真空度 (mbar)
|
|
||||||
self._min_vacuum: float = 1.0 # 最小真空度 (mbar)
|
|
||||||
self._max_vacuum: float = 1013.25 # 最大真空度 (mbar) - 大气压
|
|
||||||
|
|
||||||
# 泵性能相关属性
|
|
||||||
self._pump_speed: float = 0.0 # 泵速 (L/s)
|
|
||||||
self._max_pump_speed: float = 100.0 # 最大泵速 (L/s)
|
|
||||||
self._pump_efficiency: float = 95.0 # 泵效率百分比
|
|
||||||
|
|
||||||
# 运行控制线程
|
|
||||||
self._vacuum_thread = None
|
|
||||||
self._running = False
|
|
||||||
self._thread_lock = threading.Lock()
|
|
||||||
|
|
||||||
# ==================== 状态属性 ====================
|
|
||||||
# 这些属性会被Uni-Lab系统自动识别并定时对外广播
|
|
||||||
|
|
||||||
@property
|
|
||||||
def status(self) -> str:
|
|
||||||
"""
|
|
||||||
设备状态 - 会被自动识别的设备属性
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 当前设备状态 (Idle, Running, Error, Stopped)
|
|
||||||
"""
|
|
||||||
return self._status
|
|
||||||
|
|
||||||
@property
|
|
||||||
def power_state(self) -> str:
|
|
||||||
"""
|
|
||||||
电源状态
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 电源状态 (On, Off)
|
|
||||||
"""
|
|
||||||
return self._power_state
|
|
||||||
|
|
||||||
@property
|
|
||||||
def pump_state(self) -> str:
|
|
||||||
"""
|
|
||||||
泵运行状态
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 泵状态 (Running, Stopped, Paused)
|
|
||||||
"""
|
|
||||||
return self._pump_state
|
|
||||||
|
|
||||||
@property
|
|
||||||
def vacuum_level(self) -> float:
|
|
||||||
"""
|
|
||||||
当前真空度
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: 当前真空度 (mbar)
|
|
||||||
"""
|
|
||||||
return self._vacuum_level
|
|
||||||
|
|
||||||
@property
|
|
||||||
def target_vacuum(self) -> float:
|
|
||||||
"""
|
|
||||||
目标真空度
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: 目标真空度 (mbar)
|
|
||||||
"""
|
|
||||||
return self._target_vacuum
|
|
||||||
|
|
||||||
@property
|
|
||||||
def pump_speed(self) -> float:
|
|
||||||
"""
|
|
||||||
泵速
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: 泵速 (L/s)
|
|
||||||
"""
|
|
||||||
return self._pump_speed
|
|
||||||
|
|
||||||
@property
|
|
||||||
def pump_efficiency(self) -> float:
|
|
||||||
"""
|
|
||||||
泵效率
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: 泵效率百分比
|
|
||||||
"""
|
|
||||||
return self._pump_efficiency
|
|
||||||
|
|
||||||
@property
|
|
||||||
def max_pump_speed(self) -> float:
|
|
||||||
"""
|
|
||||||
最大泵速
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float: 最大泵速 (L/s)
|
|
||||||
"""
|
|
||||||
return self._max_pump_speed
|
|
||||||
|
|
||||||
# ==================== 设备控制方法 ====================
|
|
||||||
# 这些方法需要在注册表中添加,会作为ActionServer接受控制指令
|
|
||||||
|
|
||||||
def power_control(self, power_state: str = "On") -> str:
|
|
||||||
"""
|
|
||||||
电源控制方法
|
|
||||||
|
|
||||||
Args:
|
|
||||||
power_state (str): 电源状态,可选值:"On", "Off"
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 操作结果状态 ("Success", "Error")
|
|
||||||
"""
|
|
||||||
if power_state not in ["On", "Off"]:
|
|
||||||
self._status = "Error: Invalid power state"
|
|
||||||
return "Error"
|
|
||||||
|
|
||||||
self._power_state = power_state
|
|
||||||
|
|
||||||
if power_state == "On":
|
|
||||||
self._status = "Power On"
|
|
||||||
self._start_vacuum_operation()
|
|
||||||
else:
|
|
||||||
self._status = "Power Off"
|
|
||||||
self.stop_vacuum()
|
|
||||||
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
def set_vacuum_level(self, vacuum_level: float) -> str:
|
|
||||||
"""
|
|
||||||
设置目标真空度
|
|
||||||
|
|
||||||
Args:
|
|
||||||
vacuum_level (float): 目标真空度 (mbar)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 操作结果状态 ("Success", "Error")
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
vacuum_level = float(vacuum_level)
|
|
||||||
except ValueError:
|
|
||||||
self._status = "Error: Invalid vacuum level"
|
|
||||||
return "Error"
|
|
||||||
if self._power_state != "On":
|
|
||||||
self._status = "Error: Power Off"
|
|
||||||
return "Error"
|
|
||||||
|
|
||||||
if vacuum_level < self._min_vacuum or vacuum_level > self._max_vacuum:
|
|
||||||
self._status = f"Error: Vacuum level out of range ({self._min_vacuum}-{self._max_vacuum})"
|
|
||||||
return "Error"
|
|
||||||
|
|
||||||
self._target_vacuum = vacuum_level
|
|
||||||
self._status = "Setting Vacuum Level"
|
|
||||||
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
def start_vacuum(self) -> str:
|
|
||||||
"""
|
|
||||||
启动真空泵
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 操作结果状态 ("Success", "Error")
|
|
||||||
"""
|
|
||||||
if self._power_state != "On":
|
|
||||||
self._status = "Error: Power Off"
|
|
||||||
return "Error"
|
|
||||||
|
|
||||||
self._pump_state = "Running"
|
|
||||||
self._status = "Starting Vacuum Pump"
|
|
||||||
self._start_vacuum_operation()
|
|
||||||
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
def stop_vacuum(self) -> str:
|
|
||||||
"""
|
|
||||||
停止真空泵
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 操作结果状态 ("Success", "Error")
|
|
||||||
"""
|
|
||||||
self._pump_state = "Stopped"
|
|
||||||
self._status = "Stopping Vacuum Pump"
|
|
||||||
self._stop_vacuum_operation()
|
|
||||||
self._pump_speed = 0.0
|
|
||||||
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
def pause_vacuum(self) -> str:
|
|
||||||
"""
|
|
||||||
暂停真空泵
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 操作结果状态 ("Success", "Error")
|
|
||||||
"""
|
|
||||||
if self._pump_state != "Running":
|
|
||||||
self._status = "Error: Pump not running"
|
|
||||||
return "Error"
|
|
||||||
|
|
||||||
self._pump_state = "Paused"
|
|
||||||
self._status = "Vacuum Pump Paused"
|
|
||||||
self._stop_vacuum_operation()
|
|
||||||
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
def resume_vacuum(self) -> str:
|
|
||||||
"""
|
|
||||||
恢复真空泵运行
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 操作结果状态 ("Success", "Error")
|
|
||||||
"""
|
|
||||||
if self._pump_state != "Paused":
|
|
||||||
self._status = "Error: Pump not paused"
|
|
||||||
return "Error"
|
|
||||||
|
|
||||||
if self._power_state != "On":
|
|
||||||
self._status = "Error: Power Off"
|
|
||||||
return "Error"
|
|
||||||
|
|
||||||
self._pump_state = "Running"
|
|
||||||
self._status = "Resuming Vacuum Pump"
|
|
||||||
self._start_vacuum_operation()
|
|
||||||
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
def vent_to_atmosphere(self) -> str:
|
|
||||||
"""
|
|
||||||
通大气 - 将真空度恢复到大气压
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 操作结果状态 ("Success", "Error")
|
|
||||||
"""
|
|
||||||
self._target_vacuum = self._max_vacuum # 设置为大气压
|
|
||||||
self._status = "Venting to Atmosphere"
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
def emergency_stop(self) -> str:
|
|
||||||
"""
|
|
||||||
紧急停止
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: 操作结果状态 ("Success", "Error")
|
|
||||||
"""
|
|
||||||
self._status = "Emergency Stop"
|
|
||||||
self._pump_state = "Stopped"
|
|
||||||
self._stop_vacuum_operation()
|
|
||||||
self._pump_speed = 0.0
|
|
||||||
|
|
||||||
return "Success"
|
|
||||||
|
|
||||||
# ==================== 内部控制方法 ====================
|
|
||||||
|
|
||||||
def _start_vacuum_operation(self):
|
|
||||||
"""
|
|
||||||
启动真空操作线程
|
|
||||||
|
|
||||||
这个方法启动一个后台线程来模拟真空泵的实际运行过程。
|
|
||||||
"""
|
|
||||||
with self._thread_lock:
|
|
||||||
if not self._running and self._power_state == "On":
|
|
||||||
self._running = True
|
|
||||||
self._vacuum_thread = threading.Thread(target=self._vacuum_operation_loop)
|
|
||||||
self._vacuum_thread.daemon = True
|
|
||||||
self._vacuum_thread.start()
|
|
||||||
|
|
||||||
def _stop_vacuum_operation(self):
|
|
||||||
"""
|
|
||||||
停止真空操作线程
|
|
||||||
|
|
||||||
安全地停止后台运行线程并等待其完成。
|
|
||||||
"""
|
|
||||||
with self._thread_lock:
|
|
||||||
self._running = False
|
|
||||||
if self._vacuum_thread and self._vacuum_thread.is_alive():
|
|
||||||
self._vacuum_thread.join(timeout=2.0)
|
|
||||||
|
|
||||||
def _vacuum_operation_loop(self):
|
|
||||||
"""
|
|
||||||
真空操作主循环
|
|
||||||
|
|
||||||
这个方法在后台线程中运行,模拟真空泵的工作过程:
|
|
||||||
1. 检查电源状态和运行状态
|
|
||||||
2. 如果泵状态为 "Running",根据目标真空调整泵速和真空度
|
|
||||||
3. 否则等待
|
|
||||||
"""
|
|
||||||
while self._running and self._power_state == "On":
|
|
||||||
try:
|
|
||||||
with self._thread_lock:
|
|
||||||
# 只有泵状态为 Running 时才进行更新
|
|
||||||
if self._pump_state == "Running":
|
|
||||||
vacuum_diff = self._vacuum_level - self._target_vacuum
|
|
||||||
|
|
||||||
if abs(vacuum_diff) < 1.0: # 真空度接近目标值
|
|
||||||
self._status = "At Target Vacuum"
|
|
||||||
self._pump_speed = self._max_pump_speed * 0.2 # 维持真空的最小泵速
|
|
||||||
elif vacuum_diff > 0: # 需要抽真空(降低压力)
|
|
||||||
self._status = "Pumping Down"
|
|
||||||
if vacuum_diff > 500:
|
|
||||||
self._pump_speed = self._max_pump_speed
|
|
||||||
elif vacuum_diff > 100:
|
|
||||||
self._pump_speed = self._max_pump_speed * 0.8
|
|
||||||
elif vacuum_diff > 50:
|
|
||||||
self._pump_speed = self._max_pump_speed * 0.6
|
|
||||||
else:
|
|
||||||
self._pump_speed = self._max_pump_speed * 0.4
|
|
||||||
|
|
||||||
# 根据泵速和效率计算真空降幅
|
|
||||||
pump_rate = (self._pump_speed / self._max_pump_speed) * self._pump_efficiency / 100.0
|
|
||||||
vacuum_reduction = pump_rate * 10.0 # 每秒最大降低10 mbar
|
|
||||||
self._vacuum_level = max(self._target_vacuum, self._vacuum_level - vacuum_reduction)
|
|
||||||
else: # 目标真空度高于当前值,需要通气
|
|
||||||
self._status = "Venting"
|
|
||||||
self._pump_speed = 0.0
|
|
||||||
self._vacuum_level = min(self._target_vacuum, self._vacuum_level + 5.0)
|
|
||||||
|
|
||||||
# 限制真空度范围
|
|
||||||
self._vacuum_level = max(self._min_vacuum, min(self._max_vacuum, self._vacuum_level))
|
|
||||||
else:
|
|
||||||
# 当泵状态不是 Running 时,可保持原状态
|
|
||||||
self._status = "Vacuum Pump Not Running"
|
|
||||||
# 释放锁后等待1秒钟
|
|
||||||
time.sleep(1.0)
|
|
||||||
except Exception as e:
|
|
||||||
with self._thread_lock:
|
|
||||||
self._status = f"Error in vacuum operation: {str(e)}"
|
|
||||||
break
|
|
||||||
|
|
||||||
# 循环结束后的清理工作
|
|
||||||
if self._pump_state == "Running":
|
|
||||||
self._status = "Idle"
|
|
||||||
# 停止泵后,真空度逐渐回升到大气压
|
|
||||||
while self._vacuum_level < self._max_vacuum * 0.9:
|
|
||||||
with self._thread_lock:
|
|
||||||
self._vacuum_level += 2.0
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
def get_status_info(self) -> dict:
|
|
||||||
"""
|
|
||||||
获取完整的设备状态信息
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: 包含所有设备状态的字典
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
"status": self._status,
|
|
||||||
"power_state": self._power_state,
|
|
||||||
"pump_state": self._pump_state,
|
|
||||||
"vacuum_level": self._vacuum_level,
|
|
||||||
"target_vacuum": self._target_vacuum,
|
|
||||||
"pump_speed": self._pump_speed,
|
|
||||||
"pump_efficiency": self._pump_efficiency,
|
|
||||||
"max_pump_speed": self._max_pump_speed,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# 用于测试的主函数
|
|
||||||
if __name__ == "__main__":
|
|
||||||
vacuum = MockVacuum()
|
|
||||||
|
|
||||||
# 测试基本功能
|
|
||||||
print("启动真空泵测试...")
|
|
||||||
vacuum.power_control("On")
|
|
||||||
print(f"初始状态: {vacuum.get_status_info()}")
|
|
||||||
|
|
||||||
# 设置目标真空度并启动
|
|
||||||
vacuum.set_vacuum_level(10.0) # 设置为10mbar
|
|
||||||
vacuum.start_vacuum()
|
|
||||||
|
|
||||||
# 模拟运行15秒
|
|
||||||
for i in range(15):
|
|
||||||
time.sleep(1)
|
|
||||||
print(
|
|
||||||
f"第{i+1}秒: 真空度={vacuum.vacuum_level:.1f}mbar, 泵速={vacuum.pump_speed:.1f}L/s, 状态={vacuum.status}"
|
|
||||||
)
|
|
||||||
# 测试通大气
|
|
||||||
print("测试通大气...")
|
|
||||||
vacuum.vent_to_atmosphere()
|
|
||||||
|
|
||||||
# 继续运行5秒观察通大气过程
|
|
||||||
for i in range(5):
|
|
||||||
time.sleep(1)
|
|
||||||
print(f"通大气第{i+1}秒: 真空度={vacuum.vacuum_level:.1f}mbar, 状态={vacuum.status}")
|
|
||||||
|
|
||||||
vacuum.emergency_stop()
|
|
||||||
print("测试完成")
|
|
||||||
@@ -3,6 +3,7 @@ from threading import Lock, Event
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import time
|
import time
|
||||||
|
import traceback
|
||||||
from typing import Any, Union, Optional, overload
|
from typing import Any, Union, Optional, overload
|
||||||
|
|
||||||
import serial.tools.list_ports
|
import serial.tools.list_ports
|
||||||
@@ -386,3 +387,8 @@ class RunzeSyringePump:
|
|||||||
def list():
|
def list():
|
||||||
for item in serial.tools.list_ports.comports():
|
for item in serial.tools.list_ports.comports():
|
||||||
yield RunzeSyringePumpInfo(port=item.device)
|
yield RunzeSyringePumpInfo(port=item.device)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
r = RunzeSyringePump("/dev/tty.usbserial-D30JUGG5", "1", 25.0)
|
||||||
|
r.initialize()
|
||||||
282
unilabos/devices/separator/chinwe.py
Normal file
282
unilabos/devices/separator/chinwe.py
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import serial
|
||||||
|
import serial.tools.list_ports
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
from typing import Optional, List, Dict, Tuple
|
||||||
|
|
||||||
|
class ChinweDevice:
|
||||||
|
"""
|
||||||
|
ChinWe设备控制类
|
||||||
|
提供串口通信、电机控制、传感器数据读取等功能
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, port: str, baudrate: int = 115200, debug: bool = False):
|
||||||
|
"""
|
||||||
|
初始化ChinWe设备
|
||||||
|
|
||||||
|
Args:
|
||||||
|
port: 串口名称,如果为None则自动检测
|
||||||
|
baudrate: 波特率,默认115200
|
||||||
|
"""
|
||||||
|
self.debug = debug
|
||||||
|
self.port = port
|
||||||
|
self.baudrate = baudrate
|
||||||
|
self.serial_port: Optional[serial.Serial] = None
|
||||||
|
self._voltage: float = 0.0
|
||||||
|
self._ec_value: float = 0.0
|
||||||
|
self._ec_adc_value: int = 0
|
||||||
|
self._is_connected = False
|
||||||
|
self.connect()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_connected(self) -> bool:
|
||||||
|
"""获取连接状态"""
|
||||||
|
return self._is_connected and self.serial_port and self.serial_port.is_open
|
||||||
|
|
||||||
|
@property
|
||||||
|
def voltage(self) -> float:
|
||||||
|
"""获取电源电压值"""
|
||||||
|
return self._voltage
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ec_value(self) -> float:
|
||||||
|
"""获取电导率值 (ms/cm)"""
|
||||||
|
return self._ec_value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ec_adc_value(self) -> int:
|
||||||
|
"""获取EC ADC原始值"""
|
||||||
|
return self._ec_adc_value
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_status(self) -> Dict[str, any]:
|
||||||
|
"""
|
||||||
|
获取设备状态信息
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
包含设备状态的字典
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"connected": self.is_connected,
|
||||||
|
"port": self.port,
|
||||||
|
"baudrate": self.baudrate,
|
||||||
|
"voltage": self.voltage,
|
||||||
|
"ec_value": self.ec_value,
|
||||||
|
"ec_adc_value": self.ec_adc_value
|
||||||
|
}
|
||||||
|
|
||||||
|
def connect(self, port: Optional[str] = None, baudrate: Optional[int] = None) -> bool:
|
||||||
|
"""
|
||||||
|
连接到串口设备
|
||||||
|
|
||||||
|
Args:
|
||||||
|
port: 串口名称,如果为None则使用初始化时的port或自动检测
|
||||||
|
baudrate: 波特率,如果为None则使用初始化时的baudrate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
连接是否成功
|
||||||
|
"""
|
||||||
|
if self.is_connected:
|
||||||
|
return True
|
||||||
|
|
||||||
|
target_port = port or self.port
|
||||||
|
target_baudrate = baudrate or self.baudrate
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.serial_port = serial.Serial(target_port, target_baudrate, timeout=0.5)
|
||||||
|
self._is_connected = True
|
||||||
|
self.port = target_port
|
||||||
|
self.baudrate = target_baudrate
|
||||||
|
connect_allow_times = 5
|
||||||
|
while not self.serial_port.is_open and connect_allow_times > 0:
|
||||||
|
time.sleep(0.5)
|
||||||
|
connect_allow_times -= 1
|
||||||
|
print(f"尝试连接到 {target_port} @ {target_baudrate},剩余尝试次数: {connect_allow_times}", self.debug)
|
||||||
|
raise ValueError("串口未打开,请检查设备连接")
|
||||||
|
print(f"已连接到 {target_port} @ {target_baudrate}", self.debug)
|
||||||
|
threading.Thread(target=self._read_data, daemon=True).start()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ChinweDevice连接失败: {e}")
|
||||||
|
self._is_connected = False
|
||||||
|
return False
|
||||||
|
|
||||||
|
def disconnect(self) -> bool:
|
||||||
|
"""
|
||||||
|
断开串口连接
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
断开是否成功
|
||||||
|
"""
|
||||||
|
if self.serial_port and self.serial_port.is_open:
|
||||||
|
try:
|
||||||
|
self.serial_port.close()
|
||||||
|
self._is_connected = False
|
||||||
|
print("已断开串口连接")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"断开连接失败: {e}")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _send_motor_command(self, command: str) -> bool:
|
||||||
|
"""
|
||||||
|
发送电机控制命令
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command: 电机命令字符串,例如 "M 1 CW 1.5"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
发送是否成功
|
||||||
|
"""
|
||||||
|
if not self.is_connected:
|
||||||
|
print("设备未连接")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.serial_port.write((command + "\n").encode('utf-8'))
|
||||||
|
print(f"发送命令: {command}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"发送命令失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def rotate_motor(self, motor_id: int, turns: float, clockwise: bool = True) -> bool:
|
||||||
|
"""
|
||||||
|
使电机转动指定圈数
|
||||||
|
|
||||||
|
Args:
|
||||||
|
motor_id: 电机ID(1, 2, 3...)
|
||||||
|
turns: 转动圈数,支持小数
|
||||||
|
clockwise: True为顺时针,False为逆时针
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
命令发送是否成功
|
||||||
|
"""
|
||||||
|
if clockwise:
|
||||||
|
command = f"M {motor_id} CW {turns}"
|
||||||
|
else:
|
||||||
|
command = f"M {motor_id} CCW {turns}"
|
||||||
|
return self._send_motor_command(command)
|
||||||
|
|
||||||
|
def set_motor_speed(self, motor_id: int, speed: float) -> bool:
|
||||||
|
"""
|
||||||
|
设置电机转速(如果设备支持)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
motor_id: 电机ID(1, 2, 3...)
|
||||||
|
speed: 转速值
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
命令发送是否成功
|
||||||
|
"""
|
||||||
|
command = f"M {motor_id} SPEED {speed}"
|
||||||
|
return self._send_motor_command(command)
|
||||||
|
|
||||||
|
def _read_data(self) -> List[str]:
|
||||||
|
"""
|
||||||
|
读取串口数据并解析
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
读取到的数据行列表
|
||||||
|
"""
|
||||||
|
print("开始读取串口数据...")
|
||||||
|
if not self.is_connected:
|
||||||
|
return []
|
||||||
|
|
||||||
|
data_lines = []
|
||||||
|
try:
|
||||||
|
while self.serial_port.in_waiting:
|
||||||
|
time.sleep(0.1) # 等待数据稳定
|
||||||
|
try:
|
||||||
|
line = self.serial_port.readline().decode('utf-8', errors='ignore').strip()
|
||||||
|
if line:
|
||||||
|
data_lines.append(line)
|
||||||
|
self._parse_sensor_data(line)
|
||||||
|
except Exception as ex:
|
||||||
|
print(f"解码数据错误: {ex}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"读取串口数据错误: {e}")
|
||||||
|
|
||||||
|
return data_lines
|
||||||
|
|
||||||
|
def _parse_sensor_data(self, line: str) -> None:
|
||||||
|
"""
|
||||||
|
解析传感器数据
|
||||||
|
|
||||||
|
Args:
|
||||||
|
line: 接收到的数据行
|
||||||
|
"""
|
||||||
|
# 解析电源电压
|
||||||
|
if "电源电压" in line:
|
||||||
|
try:
|
||||||
|
val = float(line.split(":")[1].replace("V", "").strip())
|
||||||
|
self._voltage = val
|
||||||
|
if self.debug:
|
||||||
|
print(f"电源电压更新: {val}V")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 解析电导率和ADC原始值(支持两种格式)
|
||||||
|
if "电导率" in line and "ADC原始值" in line:
|
||||||
|
try:
|
||||||
|
# 支持格式如:电导率:2.50ms/cm, ADC原始值:2052
|
||||||
|
ec_match = re.search(r"电导率[::]\s*([\d\.]+)", line)
|
||||||
|
adc_match = re.search(r"ADC原始值[::]\s*(\d+)", line)
|
||||||
|
if ec_match:
|
||||||
|
ec_val = float(ec_match.group(1))
|
||||||
|
self._ec_value = ec_val
|
||||||
|
if self.debug:
|
||||||
|
print(f"电导率更新: {ec_val:.2f} ms/cm")
|
||||||
|
if adc_match:
|
||||||
|
adc_val = int(adc_match.group(1))
|
||||||
|
self._ec_adc_value = adc_val
|
||||||
|
if self.debug:
|
||||||
|
print(f"EC ADC原始值更新: {adc_val}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# 仅电导率,无ADC原始值
|
||||||
|
elif "电导率" in line:
|
||||||
|
try:
|
||||||
|
val = float(line.split(":")[1].replace("ms/cm", "").strip())
|
||||||
|
self._ec_value = val
|
||||||
|
if self.debug:
|
||||||
|
print(f"电导率更新: {val:.2f} ms/cm")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# 仅ADC原始值(如有分开回传场景)
|
||||||
|
elif "ADC原始值" in line:
|
||||||
|
try:
|
||||||
|
adc_val = int(line.split(":")[1].strip())
|
||||||
|
self._ec_adc_value = adc_val
|
||||||
|
if self.debug:
|
||||||
|
print(f"EC ADC原始值更新: {adc_val}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def spin_when_ec_ge_0():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""测试函数"""
|
||||||
|
print("=== ChinWe设备测试 ===")
|
||||||
|
|
||||||
|
# 创建设备实例
|
||||||
|
device = ChinweDevice("/dev/tty.usbserial-A5069RR4", debug=True)
|
||||||
|
try:
|
||||||
|
# 测试5: 发送电机命令
|
||||||
|
print("\n5. 发送电机命令测试:")
|
||||||
|
print(" 5.3 使用通用函数控制电机20顺时针转2圈:")
|
||||||
|
device.rotate_motor(2, 20.0, clockwise=True)
|
||||||
|
time.sleep(0.5)
|
||||||
|
finally:
|
||||||
|
time.sleep(10)
|
||||||
|
# 测试7: 断开连接
|
||||||
|
print("\n7. 断开连接:")
|
||||||
|
device.disconnect()
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -3,6 +3,8 @@ import logging
|
|||||||
import time as time_module
|
import time as time_module
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
|
from unilabos.compile.utils.vessel_parser import get_vessel
|
||||||
|
|
||||||
|
|
||||||
class VirtualFilter:
|
class VirtualFilter:
|
||||||
"""Virtual filter device - 完全按照 Filter.action 规范 🌊"""
|
"""Virtual filter device - 完全按照 Filter.action 规范 🌊"""
|
||||||
@@ -40,7 +42,6 @@ class VirtualFilter:
|
|||||||
"progress": 0.0, # Filter.action feedback
|
"progress": 0.0, # Filter.action feedback
|
||||||
"current_temp": 25.0, # Filter.action feedback
|
"current_temp": 25.0, # Filter.action feedback
|
||||||
"filtered_volume": 0.0, # Filter.action feedback
|
"filtered_volume": 0.0, # Filter.action feedback
|
||||||
"current_status": "Ready for filtration", # Filter.action feedback
|
|
||||||
"message": "Ready for filtration"
|
"message": "Ready for filtration"
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -52,9 +53,7 @@ class VirtualFilter:
|
|||||||
self.logger.info(f"🧹 清理虚拟过滤器 {self.device_id} 🔚")
|
self.logger.info(f"🧹 清理虚拟过滤器 {self.device_id} 🔚")
|
||||||
|
|
||||||
self.data.update({
|
self.data.update({
|
||||||
"status": "Offline",
|
"status": "Offline"
|
||||||
"current_status": "System offline",
|
|
||||||
"message": "System offline"
|
|
||||||
})
|
})
|
||||||
|
|
||||||
self.logger.info(f"✅ 过滤器 {self.device_id} 清理完成 💤")
|
self.logger.info(f"✅ 过滤器 {self.device_id} 清理完成 💤")
|
||||||
@@ -62,8 +61,8 @@ class VirtualFilter:
|
|||||||
|
|
||||||
async def filter(
|
async def filter(
|
||||||
self,
|
self,
|
||||||
vessel: str,
|
vessel: dict,
|
||||||
filtrate_vessel: str = "",
|
filtrate_vessel: dict = {},
|
||||||
stir: bool = False,
|
stir: bool = False,
|
||||||
stir_speed: float = 300.0,
|
stir_speed: float = 300.0,
|
||||||
temp: float = 25.0,
|
temp: float = 25.0,
|
||||||
@@ -71,7 +70,9 @@ class VirtualFilter:
|
|||||||
volume: float = 0.0
|
volume: float = 0.0
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Execute filter action - 完全按照 Filter.action 参数 🌊"""
|
"""Execute filter action - 完全按照 Filter.action 参数 🌊"""
|
||||||
|
vessel_id, _ = get_vessel(vessel)
|
||||||
|
filtrate_vessel_id, _ = get_vessel(filtrate_vessel) if filtrate_vessel else (f"{vessel_id}_filtrate", {})
|
||||||
|
|
||||||
# 🔧 新增:温度自动调整
|
# 🔧 新增:温度自动调整
|
||||||
original_temp = temp
|
original_temp = temp
|
||||||
if temp == 0.0:
|
if temp == 0.0:
|
||||||
@@ -81,7 +82,7 @@ class VirtualFilter:
|
|||||||
temp = 4.0 # 小于4度自动设置为4度
|
temp = 4.0 # 小于4度自动设置为4度
|
||||||
self.logger.info(f"🌡️ 温度自动调整: {original_temp}°C → {temp}°C (最低温度) ❄️")
|
self.logger.info(f"🌡️ 温度自动调整: {original_temp}°C → {temp}°C (最低温度) ❄️")
|
||||||
|
|
||||||
self.logger.info(f"🌊 开始过滤操作: {vessel} → {filtrate_vessel} 🚰")
|
self.logger.info(f"🌊 开始过滤操作: {vessel_id} → {filtrate_vessel_id} 🚰")
|
||||||
self.logger.info(f" 🌪️ 搅拌: {stir} ({stir_speed} RPM)")
|
self.logger.info(f" 🌪️ 搅拌: {stir} ({stir_speed} RPM)")
|
||||||
self.logger.info(f" 🌡️ 温度: {temp}°C")
|
self.logger.info(f" 🌡️ 温度: {temp}°C")
|
||||||
self.logger.info(f" 💧 体积: {volume}mL")
|
self.logger.info(f" 💧 体积: {volume}mL")
|
||||||
@@ -93,7 +94,6 @@ class VirtualFilter:
|
|||||||
self.logger.error(f"❌ {error_msg}")
|
self.logger.error(f"❌ {error_msg}")
|
||||||
self.data.update({
|
self.data.update({
|
||||||
"status": f"Error: 温度超出范围 ⚠️",
|
"status": f"Error: 温度超出范围 ⚠️",
|
||||||
"current_status": f"Error: 温度超出范围 ⚠️",
|
|
||||||
"message": error_msg
|
"message": error_msg
|
||||||
})
|
})
|
||||||
return False
|
return False
|
||||||
@@ -103,7 +103,6 @@ class VirtualFilter:
|
|||||||
self.logger.error(f"❌ {error_msg}")
|
self.logger.error(f"❌ {error_msg}")
|
||||||
self.data.update({
|
self.data.update({
|
||||||
"status": f"Error: 搅拌速度超出范围 ⚠️",
|
"status": f"Error: 搅拌速度超出范围 ⚠️",
|
||||||
"current_status": f"Error: 搅拌速度超出范围 ⚠️",
|
|
||||||
"message": error_msg
|
"message": error_msg
|
||||||
})
|
})
|
||||||
return False
|
return False
|
||||||
@@ -112,8 +111,7 @@ class VirtualFilter:
|
|||||||
error_msg = f"💧 过滤体积 {volume} mL 超出范围 (0-{self._max_volume} mL) ⚠️"
|
error_msg = f"💧 过滤体积 {volume} mL 超出范围 (0-{self._max_volume} mL) ⚠️"
|
||||||
self.logger.error(f"❌ {error_msg}")
|
self.logger.error(f"❌ {error_msg}")
|
||||||
self.data.update({
|
self.data.update({
|
||||||
"status": f"Error: 体积超出范围 ⚠️",
|
"status": f"Error",
|
||||||
"current_status": f"Error: 体积超出范围 ⚠️",
|
|
||||||
"message": error_msg
|
"message": error_msg
|
||||||
})
|
})
|
||||||
return False
|
return False
|
||||||
@@ -123,12 +121,11 @@ class VirtualFilter:
|
|||||||
self.logger.info(f"🚀 开始过滤 {filter_volume}mL 液体 💧")
|
self.logger.info(f"🚀 开始过滤 {filter_volume}mL 液体 💧")
|
||||||
|
|
||||||
self.data.update({
|
self.data.update({
|
||||||
"status": f"🌊 过滤中: {vessel}",
|
"status": f"Running",
|
||||||
"current_temp": temp,
|
"current_temp": temp,
|
||||||
"filtered_volume": 0.0,
|
"filtered_volume": 0.0,
|
||||||
"progress": 0.0,
|
"progress": 0.0,
|
||||||
"current_status": f"🌊 Filtering {vessel} → {filtrate_vessel}",
|
"message": f"🚀 Starting filtration: {vessel_id} → {filtrate_vessel_id}"
|
||||||
"message": f"🚀 Starting filtration: {vessel} → {filtrate_vessel}"
|
|
||||||
})
|
})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -164,8 +161,7 @@ class VirtualFilter:
|
|||||||
"progress": progress, # Filter.action feedback
|
"progress": progress, # Filter.action feedback
|
||||||
"current_temp": temp, # Filter.action feedback
|
"current_temp": temp, # Filter.action feedback
|
||||||
"filtered_volume": current_filtered, # Filter.action feedback
|
"filtered_volume": current_filtered, # Filter.action feedback
|
||||||
"current_status": f"🌊 Filtering: {progress:.1f}% complete", # Filter.action feedback
|
"status": "Running",
|
||||||
"status": status_msg,
|
|
||||||
"message": f"🌊 Filtering: {progress:.1f}% complete, {current_filtered:.1f}mL filtered"
|
"message": f"🌊 Filtering: {progress:.1f}% complete, {current_filtered:.1f}mL filtered"
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -190,11 +186,10 @@ class VirtualFilter:
|
|||||||
"progress": 100.0, # Filter.action feedback
|
"progress": 100.0, # Filter.action feedback
|
||||||
"current_temp": final_temp, # Filter.action feedback
|
"current_temp": final_temp, # Filter.action feedback
|
||||||
"filtered_volume": filter_volume, # Filter.action feedback
|
"filtered_volume": filter_volume, # Filter.action feedback
|
||||||
"current_status": f"✅ Filtration completed: {filter_volume}mL", # Filter.action feedback
|
"message": f"✅ Filtration completed: {filter_volume}mL filtered from {vessel_id}"
|
||||||
"message": f"✅ Filtration completed: {filter_volume}mL filtered from {vessel}"
|
|
||||||
})
|
})
|
||||||
|
|
||||||
self.logger.info(f"🎉 过滤完成! 💧 {filter_volume}mL 从 {vessel} 过滤到 {filtrate_vessel} ✨")
|
self.logger.info(f"🎉 过滤完成! 💧 {filter_volume}mL 从 {vessel_id} 过滤到 {filtrate_vessel_id} ✨")
|
||||||
self.logger.info(f"📊 最终状态: 温度 {final_temp}°C | 进度 100% | 体积 {filter_volume}mL 🏁")
|
self.logger.info(f"📊 最终状态: 温度 {final_temp}°C | 进度 100% | 体积 {filter_volume}mL 🏁")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -202,8 +197,7 @@ class VirtualFilter:
|
|||||||
error_msg = f"过滤过程中发生错误: {str(e)} 💥"
|
error_msg = f"过滤过程中发生错误: {str(e)} 💥"
|
||||||
self.logger.error(f"❌ {error_msg}")
|
self.logger.error(f"❌ {error_msg}")
|
||||||
self.data.update({
|
self.data.update({
|
||||||
"status": f"❌ 过滤错误: {str(e)}",
|
"status": f"Error",
|
||||||
"current_status": f"❌ Filtration failed: {str(e)}",
|
|
||||||
"message": f"❌ Filtration failed: {str(e)}"
|
"message": f"❌ Filtration failed: {str(e)}"
|
||||||
})
|
})
|
||||||
return False
|
return False
|
||||||
@@ -222,17 +216,17 @@ class VirtualFilter:
|
|||||||
def current_temp(self) -> float:
|
def current_temp(self) -> float:
|
||||||
"""Filter.action feedback 字段 🌡️"""
|
"""Filter.action feedback 字段 🌡️"""
|
||||||
return self.data.get("current_temp", 25.0)
|
return self.data.get("current_temp", 25.0)
|
||||||
|
|
||||||
@property
|
|
||||||
def filtered_volume(self) -> float:
|
|
||||||
"""Filter.action feedback 字段 💧"""
|
|
||||||
return self.data.get("filtered_volume", 0.0)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_status(self) -> str:
|
def current_status(self) -> str:
|
||||||
"""Filter.action feedback 字段 📋"""
|
"""Filter.action feedback 字段 📋"""
|
||||||
return self.data.get("current_status", "")
|
return self.data.get("current_status", "")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filtered_volume(self) -> float:
|
||||||
|
"""Filter.action feedback 字段 💧"""
|
||||||
|
return self.data.get("filtered_volume", 0.0)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def message(self) -> str:
|
def message(self) -> str:
|
||||||
return self.data.get("message", "")
|
return self.data.get("message", "")
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ class VirtualHeatChill:
|
|||||||
self.logger.info(f"✅ 温控设备 {self.device_id} 清理完成 💤")
|
self.logger.info(f"✅ 温控设备 {self.device_id} 清理完成 💤")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def heat_chill(self, vessel: str, temp: float, time, stir: bool,
|
async def heat_chill(self, temp: float, time, stir: bool,
|
||||||
stir_speed: float, purpose: str) -> bool:
|
stir_speed: float, purpose: str, vessel: dict = {}) -> bool:
|
||||||
"""Execute heat chill action - 🔧 修复:确保参数类型正确"""
|
"""Execute heat chill action - 🔧 修复:确保参数类型正确"""
|
||||||
|
|
||||||
# 🔧 关键修复:确保所有参数类型正确
|
# 🔧 关键修复:确保所有参数类型正确
|
||||||
@@ -77,7 +77,6 @@ class VirtualHeatChill:
|
|||||||
time_value = float(time) # 强制转换为浮点数
|
time_value = float(time) # 强制转换为浮点数
|
||||||
stir_speed = float(stir_speed)
|
stir_speed = float(stir_speed)
|
||||||
stir = bool(stir)
|
stir = bool(stir)
|
||||||
vessel = str(vessel)
|
|
||||||
purpose = str(purpose)
|
purpose = str(purpose)
|
||||||
except (ValueError, TypeError) as e:
|
except (ValueError, TypeError) as e:
|
||||||
error_msg = f"参数类型转换错误: temp={temp}({type(temp)}), time={time}({type(time)}), error={str(e)}"
|
error_msg = f"参数类型转换错误: temp={temp}({type(temp)}), time={time}({type(time)}), error={str(e)}"
|
||||||
@@ -102,8 +101,7 @@ class VirtualHeatChill:
|
|||||||
operation_mode = "Maintaining"
|
operation_mode = "Maintaining"
|
||||||
status_action = "保温"
|
status_action = "保温"
|
||||||
|
|
||||||
self.logger.info(f"🌡️ 开始温控操作: {vessel} → {temp}°C {temp_emoji}")
|
self.logger.info(f"🌡️ 开始温控操作: {temp}°C {temp_emoji}")
|
||||||
self.logger.info(f" 🥽 容器: {vessel}")
|
|
||||||
self.logger.info(f" 🎯 目标温度: {temp}°C {temp_emoji}")
|
self.logger.info(f" 🎯 目标温度: {temp}°C {temp_emoji}")
|
||||||
self.logger.info(f" ⏰ 持续时间: {time_value}s")
|
self.logger.info(f" ⏰ 持续时间: {time_value}s")
|
||||||
self.logger.info(f" 🌪️ 搅拌: {stir} ({stir_speed} RPM)")
|
self.logger.info(f" 🌪️ 搅拌: {stir} ({stir_speed} RPM)")
|
||||||
@@ -147,7 +145,7 @@ class VirtualHeatChill:
|
|||||||
stir_info = f" | 🌪️ 搅拌: {stir_speed} RPM" if stir else ""
|
stir_info = f" | 🌪️ 搅拌: {stir_speed} RPM" if stir else ""
|
||||||
|
|
||||||
self.data.update({
|
self.data.update({
|
||||||
"status": f"{temp_emoji} 运行中: {status_action} {vessel} 至 {temp}°C | ⏰ 剩余: {total_time:.0f}s{stir_info}",
|
"status": f"{temp_emoji} 运行中: {status_action} 至 {temp}°C | ⏰ 剩余: {total_time:.0f}s{stir_info}",
|
||||||
"operation_mode": operation_mode,
|
"operation_mode": operation_mode,
|
||||||
"is_stirring": stir,
|
"is_stirring": stir,
|
||||||
"stir_speed": stir_speed if stir else 0.0,
|
"stir_speed": stir_speed if stir else 0.0,
|
||||||
@@ -165,7 +163,7 @@ class VirtualHeatChill:
|
|||||||
# 更新剩余时间和状态
|
# 更新剩余时间和状态
|
||||||
self.data.update({
|
self.data.update({
|
||||||
"remaining_time": remaining,
|
"remaining_time": remaining,
|
||||||
"status": f"{temp_emoji} 运行中: {status_action} {vessel} 至 {temp}°C | ⏰ 剩余: {remaining:.0f}s{stir_info}",
|
"status": f"{temp_emoji} 运行中: {status_action} 至 {temp}°C | ⏰ 剩余: {remaining:.0f}s{stir_info}",
|
||||||
"progress": progress
|
"progress": progress
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -185,7 +183,7 @@ class VirtualHeatChill:
|
|||||||
final_stir_info = f" | 🌪️ 搅拌: {stir_speed} RPM" if stir else ""
|
final_stir_info = f" | 🌪️ 搅拌: {stir_speed} RPM" if stir else ""
|
||||||
|
|
||||||
self.data.update({
|
self.data.update({
|
||||||
"status": f"✅ 完成: {vessel} 已达到 {temp}°C {temp_emoji} | ⏱️ 用时: {total_time:.0f}s{final_stir_info}",
|
"status": f"✅ 完成: 已达到 {temp}°C {temp_emoji} | ⏱️ 用时: {total_time:.0f}s{final_stir_info}",
|
||||||
"operation_mode": "Completed",
|
"operation_mode": "Completed",
|
||||||
"remaining_time": 0.0,
|
"remaining_time": 0.0,
|
||||||
"is_stirring": False,
|
"is_stirring": False,
|
||||||
@@ -195,7 +193,6 @@ class VirtualHeatChill:
|
|||||||
|
|
||||||
self.logger.info(f"🎉 温控操作完成! ✨")
|
self.logger.info(f"🎉 温控操作完成! ✨")
|
||||||
self.logger.info(f"📊 操作结果:")
|
self.logger.info(f"📊 操作结果:")
|
||||||
self.logger.info(f" 🥽 容器: {vessel}")
|
|
||||||
self.logger.info(f" 🌡️ 达到温度: {temp}°C {temp_emoji}")
|
self.logger.info(f" 🌡️ 达到温度: {temp}°C {temp_emoji}")
|
||||||
self.logger.info(f" ⏱️ 总用时: {total_time:.0f}s")
|
self.logger.info(f" ⏱️ 总用时: {total_time:.0f}s")
|
||||||
if stir:
|
if stir:
|
||||||
@@ -204,13 +201,12 @@ class VirtualHeatChill:
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def heat_chill_start(self, vessel: str, temp: float, purpose: str) -> bool:
|
async def heat_chill_start(self, temp: float, purpose: str, vessel: dict = {}) -> bool:
|
||||||
"""Start continuous heat chill 🔄"""
|
"""Start continuous heat chill 🔄"""
|
||||||
|
|
||||||
# 🔧 添加类型转换
|
# 🔧 添加类型转换
|
||||||
try:
|
try:
|
||||||
temp = float(temp)
|
temp = float(temp)
|
||||||
vessel = str(vessel)
|
|
||||||
purpose = str(purpose)
|
purpose = str(purpose)
|
||||||
except (ValueError, TypeError) as e:
|
except (ValueError, TypeError) as e:
|
||||||
error_msg = f"参数类型转换错误: {str(e)}"
|
error_msg = f"参数类型转换错误: {str(e)}"
|
||||||
@@ -235,8 +231,7 @@ class VirtualHeatChill:
|
|||||||
operation_mode = "Maintaining"
|
operation_mode = "Maintaining"
|
||||||
status_action = "恒温保持"
|
status_action = "恒温保持"
|
||||||
|
|
||||||
self.logger.info(f"🔄 启动持续温控: {vessel} → {temp}°C {temp_emoji}")
|
self.logger.info(f"🔄 启动持续温控: {temp}°C {temp_emoji}")
|
||||||
self.logger.info(f" 🥽 容器: {vessel}")
|
|
||||||
self.logger.info(f" 🎯 目标温度: {temp}°C {temp_emoji}")
|
self.logger.info(f" 🎯 目标温度: {temp}°C {temp_emoji}")
|
||||||
self.logger.info(f" 🔄 模式: {status_action}")
|
self.logger.info(f" 🔄 模式: {status_action}")
|
||||||
self.logger.info(f" 📝 目的: {purpose}")
|
self.logger.info(f" 📝 目的: {purpose}")
|
||||||
@@ -252,7 +247,7 @@ class VirtualHeatChill:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
self.data.update({
|
self.data.update({
|
||||||
"status": f"🔄 启动: {status_action} {vessel} 至 {temp}°C {temp_emoji} | ♾️ 持续运行",
|
"status": f"🔄 启动: {status_action} 至 {temp}°C {temp_emoji} | ♾️ 持续运行",
|
||||||
"operation_mode": operation_mode,
|
"operation_mode": operation_mode,
|
||||||
"is_stirring": False,
|
"is_stirring": False,
|
||||||
"stir_speed": 0.0,
|
"stir_speed": 0.0,
|
||||||
@@ -262,28 +257,20 @@ class VirtualHeatChill:
|
|||||||
self.logger.info(f"✅ 持续温控已启动! {temp_emoji} {status_action}模式 🚀")
|
self.logger.info(f"✅ 持续温控已启动! {temp_emoji} {status_action}模式 🚀")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def heat_chill_stop(self, vessel: str) -> bool:
|
async def heat_chill_stop(self, vessel: dict = {}) -> bool:
|
||||||
"""Stop heat chill 🛑"""
|
"""Stop heat chill 🛑"""
|
||||||
|
|
||||||
# 🔧 添加类型转换
|
self.logger.info(f"🛑 停止温控:")
|
||||||
try:
|
|
||||||
vessel = str(vessel)
|
|
||||||
except (ValueError, TypeError) as e:
|
|
||||||
error_msg = f"参数类型转换错误: {str(e)}"
|
|
||||||
self.logger.error(f"❌ {error_msg}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.logger.info(f"🛑 停止温控: {vessel}")
|
|
||||||
|
|
||||||
self.data.update({
|
self.data.update({
|
||||||
"status": f"🛑 已停止: {vessel} 温控停止",
|
"status": f"🛑 {self.device_id} 温控停止",
|
||||||
"operation_mode": "Stopped",
|
"operation_mode": "Stopped",
|
||||||
"is_stirring": False,
|
"is_stirring": False,
|
||||||
"stir_speed": 0.0,
|
"stir_speed": 0.0,
|
||||||
"remaining_time": 0.0,
|
"remaining_time": 0.0,
|
||||||
})
|
})
|
||||||
|
|
||||||
self.logger.info(f"✅ 温控设备已停止 {vessel} 的温度控制 🏁")
|
self.logger.info(f"✅ 温控设备已停止 {self.device_id} 温度控制 🏁")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 状态属性
|
# 状态属性
|
||||||
|
|||||||
@@ -21,19 +21,6 @@ class VirtualMultiwayValve:
|
|||||||
self._current_position = 0 # 默认在0号位(transfer pump位置)
|
self._current_position = 0 # 默认在0号位(transfer pump位置)
|
||||||
self._target_position = 0
|
self._target_position = 0
|
||||||
|
|
||||||
# 位置映射说明
|
|
||||||
self.position_map = {
|
|
||||||
0: "transfer_pump", # 0号位连接转移泵
|
|
||||||
1: "port_1", # 1号位
|
|
||||||
2: "port_2", # 2号位
|
|
||||||
3: "port_3", # 3号位
|
|
||||||
4: "port_4", # 4号位
|
|
||||||
5: "port_5", # 5号位
|
|
||||||
6: "port_6", # 6号位
|
|
||||||
7: "port_7", # 7号位
|
|
||||||
8: "port_8" # 8号位
|
|
||||||
}
|
|
||||||
|
|
||||||
print(f"🔄 === 虚拟多通阀门已创建 === ✨")
|
print(f"🔄 === 虚拟多通阀门已创建 === ✨")
|
||||||
print(f"🎯 端口: {port} | 📊 位置范围: 0-{self.max_positions} | 🏠 初始位置: 0 (transfer_pump)")
|
print(f"🎯 端口: {port} | 📊 位置范围: 0-{self.max_positions} | 🏠 初始位置: 0 (transfer_pump)")
|
||||||
self.logger.info(f"🔧 多通阀门初始化: 端口={port}, 最大位置={self.max_positions}")
|
self.logger.info(f"🔧 多通阀门初始化: 端口={port}, 最大位置={self.max_positions}")
|
||||||
@@ -60,7 +47,7 @@ class VirtualMultiwayValve:
|
|||||||
|
|
||||||
def get_current_port(self) -> str:
|
def get_current_port(self) -> str:
|
||||||
"""获取当前连接的端口名称 🔌"""
|
"""获取当前连接的端口名称 🔌"""
|
||||||
return self.position_map.get(self._current_position, "unknown")
|
return self._current_position
|
||||||
|
|
||||||
def set_position(self, command: Union[int, str]):
|
def set_position(self, command: Union[int, str]):
|
||||||
"""
|
"""
|
||||||
@@ -115,7 +102,7 @@ class VirtualMultiwayValve:
|
|||||||
old_position = self._current_position
|
old_position = self._current_position
|
||||||
old_port = self.get_current_port()
|
old_port = self.get_current_port()
|
||||||
|
|
||||||
self.logger.info(f"🔄 阀门切换: {old_position}({old_port}) → {pos}({self.position_map.get(pos, 'unknown')}) {pos_emoji}")
|
self.logger.info(f"🔄 阀门切换: {old_position}({old_port}) → {pos} {pos_emoji}")
|
||||||
|
|
||||||
self._status = "Busy"
|
self._status = "Busy"
|
||||||
self._valve_state = "Moving"
|
self._valve_state = "Moving"
|
||||||
@@ -190,6 +177,17 @@ class VirtualMultiwayValve:
|
|||||||
"""获取阀门位置 - 兼容性方法 📍"""
|
"""获取阀门位置 - 兼容性方法 📍"""
|
||||||
return self._current_position
|
return self._current_position
|
||||||
|
|
||||||
|
def set_valve_position(self, command: Union[int, str]):
|
||||||
|
"""
|
||||||
|
设置阀门位置 - 兼容pump_protocol调用 🎯
|
||||||
|
这是set_position的别名方法,用于兼容pump_protocol.py
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command: 目标位置 (0-8) 或位置字符串
|
||||||
|
"""
|
||||||
|
# 删除debug日志:self.logger.debug(f"🎯 兼容性调用: set_valve_position({command})")
|
||||||
|
return self.set_position(command)
|
||||||
|
|
||||||
def is_at_position(self, position: int) -> bool:
|
def is_at_position(self, position: int) -> bool:
|
||||||
"""检查是否在指定位置 🎯"""
|
"""检查是否在指定位置 🎯"""
|
||||||
result = self._current_position == position
|
result = self._current_position == position
|
||||||
@@ -210,17 +208,6 @@ class VirtualMultiwayValve:
|
|||||||
# 删除debug日志:self.logger.debug(f"🔌 端口{port_number}检查: {port_status} (当前位置: {self._current_position})")
|
# 删除debug日志:self.logger.debug(f"🔌 端口{port_number}检查: {port_status} (当前位置: {self._current_position})")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_available_positions(self) -> list:
|
|
||||||
"""获取可用位置列表 📋"""
|
|
||||||
positions = list(range(0, self.max_positions + 1))
|
|
||||||
# 删除debug日志:self.logger.debug(f"📋 可用位置: {positions}")
|
|
||||||
return positions
|
|
||||||
|
|
||||||
def get_available_ports(self) -> Dict[int, str]:
|
|
||||||
"""获取可用端口映射 🗺️"""
|
|
||||||
# 删除debug日志:self.logger.debug(f"🗺️ 端口映射: {self.position_map}")
|
|
||||||
return self.position_map.copy()
|
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
"""重置阀门到transfer pump位置(0号位)🔄"""
|
"""重置阀门到transfer pump位置(0号位)🔄"""
|
||||||
self.logger.info(f"🔄 重置阀门到泵位置...")
|
self.logger.info(f"🔄 重置阀门到泵位置...")
|
||||||
@@ -253,41 +240,12 @@ class VirtualMultiwayValve:
|
|||||||
# 删除debug日志:self.logger.debug(f"🌊 当前流路: {flow_path}")
|
# 删除debug日志:self.logger.debug(f"🌊 当前流路: {flow_path}")
|
||||||
return flow_path
|
return flow_path
|
||||||
|
|
||||||
def get_info(self) -> dict:
|
|
||||||
"""获取阀门详细信息 📊"""
|
|
||||||
info = {
|
|
||||||
"port": self.port,
|
|
||||||
"max_positions": self.max_positions,
|
|
||||||
"total_positions": self.total_positions,
|
|
||||||
"current_position": self._current_position,
|
|
||||||
"current_port": self.get_current_port(),
|
|
||||||
"target_position": self._target_position,
|
|
||||||
"status": self._status,
|
|
||||||
"valve_state": self._valve_state,
|
|
||||||
"flow_path": self.get_flow_path(),
|
|
||||||
"position_map": self.position_map
|
|
||||||
}
|
|
||||||
|
|
||||||
# 删除debug日志:self.logger.debug(f"📊 阀门信息: 位置={self._current_position}, 状态={self._status}, 端口={self.get_current_port()}")
|
|
||||||
return info
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
current_port = self.get_current_port()
|
current_port = self.get_current_port()
|
||||||
status_emoji = "✅" if self._status == "Idle" else "🔄" if self._status == "Busy" else "❌"
|
status_emoji = "✅" if self._status == "Idle" else "🔄" if self._status == "Busy" else "❌"
|
||||||
|
|
||||||
return f"🔄 VirtualMultiwayValve({status_emoji} 位置: {self._current_position}/{self.max_positions}, 端口: {current_port}, 状态: {self._status})"
|
return f"🔄 VirtualMultiwayValve({status_emoji} 位置: {self._current_position}/{self.max_positions}, 端口: {current_port}, 状态: {self._status})"
|
||||||
|
|
||||||
def set_valve_position(self, command: Union[int, str]):
|
|
||||||
"""
|
|
||||||
设置阀门位置 - 兼容pump_protocol调用 🎯
|
|
||||||
这是set_position的别名方法,用于兼容pump_protocol.py
|
|
||||||
|
|
||||||
Args:
|
|
||||||
command: 目标位置 (0-8) 或位置字符串
|
|
||||||
"""
|
|
||||||
# 删除debug日志:self.logger.debug(f"🎯 兼容性调用: set_valve_position({command})")
|
|
||||||
return self.set_position(command)
|
|
||||||
|
|
||||||
|
|
||||||
# 使用示例
|
# 使用示例
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@@ -309,13 +267,6 @@ if __name__ == "__main__":
|
|||||||
print(f"\n🔌 切换到2号位: {valve.set_to_port(2)}")
|
print(f"\n🔌 切换到2号位: {valve.set_to_port(2)}")
|
||||||
print(f"📍 当前状态: {valve}")
|
print(f"📍 当前状态: {valve}")
|
||||||
|
|
||||||
# 显示所有可用位置
|
|
||||||
print(f"\n📋 可用位置: {valve.get_available_positions()}")
|
|
||||||
print(f"🗺️ 端口映射: {valve.get_available_ports()}")
|
|
||||||
|
|
||||||
# 获取详细信息
|
|
||||||
print(f"\n📊 详细信息: {valve.get_info()}")
|
|
||||||
|
|
||||||
# 测试切换功能
|
# 测试切换功能
|
||||||
print(f"\n🔄 智能切换测试:")
|
print(f"\n🔄 智能切换测试:")
|
||||||
print(f"当前位置: {valve._current_position}")
|
print(f"当前位置: {valve._current_position}")
|
||||||
|
|||||||
@@ -1,197 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
from typing import Dict, Any, Optional
|
|
||||||
|
|
||||||
class VirtualPump:
|
|
||||||
"""Virtual pump device for transfer and cleaning operations"""
|
|
||||||
|
|
||||||
def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
|
|
||||||
# 处理可能的不同调用方式
|
|
||||||
if device_id is None and 'id' in kwargs:
|
|
||||||
device_id = kwargs.pop('id')
|
|
||||||
if config is None and 'config' in kwargs:
|
|
||||||
config = kwargs.pop('config')
|
|
||||||
|
|
||||||
# 设置默认值
|
|
||||||
self.device_id = device_id or "unknown_pump"
|
|
||||||
self.config = config or {}
|
|
||||||
|
|
||||||
self.logger = logging.getLogger(f"VirtualPump.{self.device_id}")
|
|
||||||
self.data = {}
|
|
||||||
|
|
||||||
# 从config或kwargs中获取配置参数
|
|
||||||
self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL')
|
|
||||||
self._max_volume = self.config.get('max_volume') or kwargs.get('max_volume', 50.0)
|
|
||||||
self._transfer_rate = self.config.get('transfer_rate') or kwargs.get('transfer_rate', 10.0)
|
|
||||||
|
|
||||||
print(f"=== VirtualPump {self.device_id} created with max_volume={self._max_volume}, transfer_rate={self._transfer_rate} ===")
|
|
||||||
|
|
||||||
async def initialize(self) -> bool:
|
|
||||||
"""Initialize virtual pump"""
|
|
||||||
self.logger.info(f"Initializing virtual pump {self.device_id}")
|
|
||||||
self.data.update({
|
|
||||||
"status": "Idle",
|
|
||||||
"valve_position": 0,
|
|
||||||
"current_volume": 0.0,
|
|
||||||
"max_volume": self._max_volume,
|
|
||||||
"transfer_rate": self._transfer_rate,
|
|
||||||
"from_vessel": "",
|
|
||||||
"to_vessel": "",
|
|
||||||
"progress": 0.0,
|
|
||||||
"transferred_volume": 0.0,
|
|
||||||
"current_status": "Ready"
|
|
||||||
})
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def cleanup(self) -> bool:
|
|
||||||
"""Cleanup virtual pump"""
|
|
||||||
self.logger.info(f"Cleaning up virtual pump {self.device_id}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def transfer(self, from_vessel: str, to_vessel: str, volume: float,
|
|
||||||
amount: str = "", time: float = 0.0, viscous: bool = False,
|
|
||||||
rinsing_solvent: str = "", rinsing_volume: float = 0.0,
|
|
||||||
rinsing_repeats: int = 0, solid: bool = False) -> bool:
|
|
||||||
"""Execute transfer operation"""
|
|
||||||
self.logger.info(f"Transferring {volume}mL from {from_vessel} to {to_vessel}")
|
|
||||||
|
|
||||||
# 计算转移时间
|
|
||||||
transfer_time = volume / self._transfer_rate if time == 0 else time
|
|
||||||
|
|
||||||
self.data.update({
|
|
||||||
"status": "Running",
|
|
||||||
"from_vessel": from_vessel,
|
|
||||||
"to_vessel": to_vessel,
|
|
||||||
"current_status": "Transferring",
|
|
||||||
"progress": 0.0,
|
|
||||||
"transferred_volume": 0.0
|
|
||||||
})
|
|
||||||
|
|
||||||
# 模拟转移过程
|
|
||||||
steps = 10
|
|
||||||
step_time = transfer_time / steps
|
|
||||||
step_volume = volume / steps
|
|
||||||
|
|
||||||
for i in range(steps):
|
|
||||||
await asyncio.sleep(step_time)
|
|
||||||
progress = (i + 1) / steps * 100
|
|
||||||
current_volume = step_volume * (i + 1)
|
|
||||||
|
|
||||||
self.data.update({
|
|
||||||
"progress": progress,
|
|
||||||
"transferred_volume": current_volume,
|
|
||||||
"current_status": f"Transferring: {progress:.1f}%"
|
|
||||||
})
|
|
||||||
|
|
||||||
self.logger.info(f"Transfer progress: {progress:.1f}%")
|
|
||||||
|
|
||||||
self.data.update({
|
|
||||||
"status": "Idle",
|
|
||||||
"current_status": "Transfer completed",
|
|
||||||
"progress": 100.0,
|
|
||||||
"transferred_volume": volume
|
|
||||||
})
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def clean_vessel(self, vessel: str, solvent: str, volume: float,
|
|
||||||
temp: float, repeats: int = 1) -> bool:
|
|
||||||
"""Execute vessel cleaning operation - matches CleanVessel action"""
|
|
||||||
self.logger.info(f"Starting vessel cleaning: {vessel} with {solvent} ({volume}mL at {temp}°C, {repeats} repeats)")
|
|
||||||
|
|
||||||
# 更新设备状态
|
|
||||||
self.data.update({
|
|
||||||
"status": "Running",
|
|
||||||
"from_vessel": f"flask_{solvent}",
|
|
||||||
"to_vessel": vessel,
|
|
||||||
"current_status": "Cleaning in progress",
|
|
||||||
"progress": 0.0,
|
|
||||||
"transferred_volume": 0.0
|
|
||||||
})
|
|
||||||
|
|
||||||
# 计算清洗时间(基于体积和重复次数)
|
|
||||||
# 假设清洗速度为 transfer_rate 的一半(因为需要加载和排放)
|
|
||||||
cleaning_rate = self._transfer_rate / 2
|
|
||||||
cleaning_time_per_cycle = volume / cleaning_rate
|
|
||||||
total_cleaning_time = cleaning_time_per_cycle * repeats
|
|
||||||
|
|
||||||
# 模拟清洗过程
|
|
||||||
steps_per_repeat = 10 # 每次重复清洗分10个步骤
|
|
||||||
total_steps = steps_per_repeat * repeats
|
|
||||||
step_time = total_cleaning_time / total_steps
|
|
||||||
|
|
||||||
for repeat in range(repeats):
|
|
||||||
self.logger.info(f"Starting cleaning cycle {repeat + 1}/{repeats}")
|
|
||||||
|
|
||||||
for step in range(steps_per_repeat):
|
|
||||||
await asyncio.sleep(step_time)
|
|
||||||
|
|
||||||
# 计算当前进度
|
|
||||||
current_step = repeat * steps_per_repeat + step + 1
|
|
||||||
progress = (current_step / total_steps) * 100
|
|
||||||
|
|
||||||
# 计算已处理的体积
|
|
||||||
volume_processed = (current_step / total_steps) * volume * repeats
|
|
||||||
|
|
||||||
# 更新状态
|
|
||||||
self.data.update({
|
|
||||||
"progress": progress,
|
|
||||||
"transferred_volume": volume_processed,
|
|
||||||
"current_status": f"Cleaning cycle {repeat + 1}/{repeats} - Step {step + 1}/{steps_per_repeat} ({progress:.1f}%)"
|
|
||||||
})
|
|
||||||
|
|
||||||
self.logger.info(f"Cleaning progress: {progress:.1f}% (Cycle {repeat + 1}/{repeats})")
|
|
||||||
|
|
||||||
# 清洗完成
|
|
||||||
self.data.update({
|
|
||||||
"status": "Idle",
|
|
||||||
"current_status": "Cleaning completed successfully",
|
|
||||||
"progress": 100.0,
|
|
||||||
"transferred_volume": volume * repeats,
|
|
||||||
"from_vessel": "",
|
|
||||||
"to_vessel": ""
|
|
||||||
})
|
|
||||||
|
|
||||||
self.logger.info(f"Vessel cleaning completed: {vessel}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
# 状态属性
|
|
||||||
@property
|
|
||||||
def status(self) -> str:
|
|
||||||
return self.data.get("status", "Unknown")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def valve_position(self) -> int:
|
|
||||||
return self.data.get("valve_position", 0)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_volume(self) -> float:
|
|
||||||
return self.data.get("current_volume", 0.0)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def max_volume(self) -> float:
|
|
||||||
return self.data.get("max_volume", 0.0)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def transfer_rate(self) -> float:
|
|
||||||
return self.data.get("transfer_rate", 0.0)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def from_vessel(self) -> str:
|
|
||||||
return self.data.get("from_vessel", "")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def to_vessel(self) -> str:
|
|
||||||
return self.data.get("to_vessel", "")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def progress(self) -> float:
|
|
||||||
return self.data.get("progress", 0.0)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def transferred_volume(self) -> float:
|
|
||||||
return self.data.get("transferred_volume", 0.0)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_status(self) -> str:
|
|
||||||
return self.data.get("current_status", "Ready")
|
|
||||||
@@ -99,8 +99,8 @@ class VirtualRotavap:
|
|||||||
self.logger.error(f"❌ 时间参数类型无效: {type(time)},使用默认值180.0秒")
|
self.logger.error(f"❌ 时间参数类型无效: {type(time)},使用默认值180.0秒")
|
||||||
time = 180.0
|
time = 180.0
|
||||||
|
|
||||||
# 确保time是float类型
|
# 确保time是float类型; 并加速
|
||||||
time = float(time)
|
time = float(time) / 10.0
|
||||||
|
|
||||||
# 🔧 简化处理:如果vessel就是设备自己,直接操作
|
# 🔧 简化处理:如果vessel就是设备自己,直接操作
|
||||||
if vessel == self.device_id:
|
if vessel == self.device_id:
|
||||||
|
|||||||
@@ -48,20 +48,6 @@ class VirtualSolenoidValve:
|
|||||||
"""获取阀门位置状态"""
|
"""获取阀门位置状态"""
|
||||||
return "OPEN" if self._is_open else "CLOSED"
|
return "OPEN" if self._is_open else "CLOSED"
|
||||||
|
|
||||||
@property
|
|
||||||
def state(self) -> dict:
|
|
||||||
"""获取阀门完整状态"""
|
|
||||||
return {
|
|
||||||
"device_id": self.device_id,
|
|
||||||
"port": self.port,
|
|
||||||
"voltage": self.voltage,
|
|
||||||
"response_time": self.response_time,
|
|
||||||
"is_open": self._is_open,
|
|
||||||
"valve_state": self._valve_state,
|
|
||||||
"status": self._status,
|
|
||||||
"position": self.valve_position
|
|
||||||
}
|
|
||||||
|
|
||||||
async def set_valve_position(self, command: str = None, **kwargs):
|
async def set_valve_position(self, command: str = None, **kwargs):
|
||||||
"""
|
"""
|
||||||
设置阀门位置 - ROS动作接口
|
设置阀门位置 - ROS动作接口
|
||||||
|
|||||||
@@ -319,21 +319,6 @@ class VirtualSolidDispenser:
|
|||||||
def total_operations(self) -> int:
|
def total_operations(self) -> int:
|
||||||
return self._total_operations
|
return self._total_operations
|
||||||
|
|
||||||
def get_device_info(self) -> Dict[str, Any]:
|
|
||||||
"""获取设备状态信息 📊"""
|
|
||||||
info = {
|
|
||||||
"device_id": self.device_id,
|
|
||||||
"status": self._status,
|
|
||||||
"current_reagent": self._current_reagent,
|
|
||||||
"last_dispensed_amount": self._dispensed_amount,
|
|
||||||
"total_operations": self._total_operations,
|
|
||||||
"max_capacity": self.max_capacity,
|
|
||||||
"precision": self.precision
|
|
||||||
}
|
|
||||||
|
|
||||||
self.logger.debug(f"📊 设备信息: 状态={self._status}, 试剂={self._current_reagent}, 加样量={self._dispensed_amount:.6f}g")
|
|
||||||
return info
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
status_emoji = "✅" if self._status == "Ready" else "🔄" if self._status == "Dispensing" else "❌" if self._status == "Error" else "🏠"
|
status_emoji = "✅" if self._status == "Ready" else "🔄" if self._status == "Dispensing" else "❌" if self._status == "Error" else "🏠"
|
||||||
return f"⚗️ VirtualSolidDispenser({status_emoji} {self.device_id}: {self._status}, 最后加样 {self._dispensed_amount:.3f}g)"
|
return f"⚗️ VirtualSolidDispenser({status_emoji} {self.device_id}: {self._status}, 最后加样 {self._dispensed_amount:.3f}g)"
|
||||||
@@ -380,8 +365,6 @@ async def test_solid_dispenser():
|
|||||||
mass="150 g" # 超过100g限制
|
mass="150 g" # 超过100g限制
|
||||||
)
|
)
|
||||||
print(f"📊 测试4结果: {result4}")
|
print(f"📊 测试4结果: {result4}")
|
||||||
|
|
||||||
print(f"\n📊 最终设备信息: {dispenser.get_device_info()}")
|
|
||||||
print(f"✅ === 测试完成 === 🎉")
|
print(f"✅ === 测试完成 === 🎉")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ class VirtualStirrer:
|
|||||||
"min_speed": self._min_speed
|
"min_speed": self._min_speed
|
||||||
}
|
}
|
||||||
|
|
||||||
self.logger.debug(f"📊 设备信息: 模式={self.operation_mode}, 速度={self.current_speed} RPM, 搅拌={self.is_stirring}")
|
# self.logger.debug(f"📊 设备信息: 模式={self.operation_mode}, 速度={self.current_speed} RPM, 搅拌={self.is_stirring}")
|
||||||
return info
|
return info
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|||||||
@@ -380,22 +380,6 @@ class VirtualTransferPump:
|
|||||||
"""检查是否已满"""
|
"""检查是否已满"""
|
||||||
return self._current_volume >= (self.max_volume - 0.01) # 允许小量误差
|
return self._current_volume >= (self.max_volume - 0.01) # 允许小量误差
|
||||||
|
|
||||||
# 调试和状态信息
|
|
||||||
def get_pump_info(self) -> dict:
|
|
||||||
"""获取泵的详细信息"""
|
|
||||||
return {
|
|
||||||
"device_id": self.device_id,
|
|
||||||
"status": self._status,
|
|
||||||
"position": self._position,
|
|
||||||
"current_volume": self._current_volume,
|
|
||||||
"max_volume": self.max_volume,
|
|
||||||
"max_velocity": self._max_velocity,
|
|
||||||
"mode": self.mode.name,
|
|
||||||
"is_empty": self.is_empty(),
|
|
||||||
"is_full": self.is_full(),
|
|
||||||
"remaining_capacity": self.get_remaining_capacity()
|
|
||||||
}
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"VirtualTransferPump({self.device_id}: {self._current_volume:.2f}/{self.max_volume} ml, {self._status})"
|
return f"VirtualTransferPump({self.device_id}: {self._current_volume:.2f}/{self.max_volume} ml, {self._status})"
|
||||||
|
|
||||||
@@ -425,8 +409,6 @@ async def demo():
|
|||||||
result = await pump.set_position(0.0)
|
result = await pump.set_position(0.0)
|
||||||
print(f"Empty result: {result}")
|
print(f"Empty result: {result}")
|
||||||
print(f"After emptying: {pump}")
|
print(f"After emptying: {pump}")
|
||||||
|
|
||||||
print("\nPump info:", pump.get_pump_info())
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
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)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
serial:
|
serial:
|
||||||
category:
|
category:
|
||||||
- serial
|
- communication_devices
|
||||||
class:
|
class:
|
||||||
action_value_mappings:
|
action_value_mappings:
|
||||||
auto-handle_serial_request:
|
auto-handle_serial_request:
|
||||||
@@ -9,7 +9,7 @@ serial:
|
|||||||
goal_default:
|
goal_default:
|
||||||
request: null
|
request: null
|
||||||
response: null
|
response: null
|
||||||
handles: []
|
handles: {}
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: handle_serial_request的参数schema
|
description: handle_serial_request的参数schema
|
||||||
@@ -35,7 +35,7 @@ serial:
|
|||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: []
|
handles: {}
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: read_data的参数schema
|
description: read_data的参数schema
|
||||||
@@ -56,7 +56,7 @@ serial:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default:
|
goal_default:
|
||||||
command: null
|
command: null
|
||||||
handles: []
|
handles: {}
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: send_command的参数schema
|
description: send_command的参数schema
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
camera:
|
camera.USB:
|
||||||
category:
|
category:
|
||||||
- camera
|
- camera
|
||||||
class:
|
class:
|
||||||
@@ -7,7 +7,7 @@ camera:
|
|||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: []
|
handles: {}
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 用于安全地关闭摄像头设备,释放摄像头资源,停止视频采集和发布服务。调用此函数将清理OpenCV摄像头连接并销毁ROS2节点。
|
description: 用于安全地关闭摄像头设备,释放摄像头资源,停止视频采集和发布服务。调用此函数将清理OpenCV摄像头连接并销毁ROS2节点。
|
||||||
@@ -27,7 +27,7 @@ camera:
|
|||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
goal_default: {}
|
goal_default: {}
|
||||||
handles: []
|
handles: {}
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 定时器回调函数的参数schema。此函数负责定期采集摄像头视频帧,将OpenCV格式的图像转换为ROS Image消息格式,并发布到指定的视频话题。默认以10Hz频率执行,确保视频流的连续性和实时性。
|
description: 定时器回调函数的参数schema。此函数负责定期采集摄像头视频帧,将OpenCV格式的图像转换为ROS Image消息格式,并发布到指定的视频话题。默认以10Hz频率执行,确保视频流的连续性和实时性。
|
||||||
|
|||||||
404
unilabos/registry/devices/characterization_chromatic.yaml
Normal file
404
unilabos/registry/devices/characterization_chromatic.yaml
Normal file
@@ -0,0 +1,404 @@
|
|||||||
|
hplc.agilent:
|
||||||
|
category:
|
||||||
|
- characterization_chromatic
|
||||||
|
class:
|
||||||
|
action_value_mappings:
|
||||||
|
auto-check_status:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 检查安捷伦HPLC设备状态的函数。用于监控设备的运行状态、连接状态、错误信息等关键指标。该函数定期查询设备状态,确保系统稳定运行,及时发现和报告设备异常。适用于自动化流程中的设备监控、故障诊断、系统维护等场景。
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: check_status参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-extract_data_from_txt:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
file_path: null
|
||||||
|
handles: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 从文本文件中提取分析数据的函数。用于解析安捷伦HPLC生成的结果文件,提取峰面积、保留时间、浓度等关键分析数据。支持多种文件格式的自动识别和数据结构化处理,为后续数据分析和报告生成提供标准化的数据格式。适用于批量数据处理、结果验证、质量控制等分析工作流程。
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
file_path:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- file_path
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: extract_data_from_txt参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-start_sequence:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
params: null
|
||||||
|
resource: null
|
||||||
|
wf_name: null
|
||||||
|
handles: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 启动安捷伦HPLC分析序列的函数。用于执行预定义的分析方法序列,包括样品进样、色谱分离、检测等完整的分析流程。支持参数配置、资源分配、工作流程管理等功能,实现全自动的样品分析。适用于批量样品处理、标准化分析、质量检测等需要连续自动分析的应用场景。
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
params:
|
||||||
|
type: string
|
||||||
|
resource:
|
||||||
|
type: object
|
||||||
|
wf_name:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- wf_name
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: start_sequence参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-try_close_sub_device:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
device_name: null
|
||||||
|
handles: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 尝试关闭HPLC子设备的函数。用于安全地关闭泵、检测器、进样器等各个子模块,确保设备正常断开连接并保护硬件安全。该函数提供错误处理和状态确认机制,避免强制关闭可能造成的设备损坏。适用于设备维护、系统重启、紧急停机等需要安全关闭设备的场景。
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
device_name:
|
||||||
|
type: string
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: try_close_sub_device参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-try_open_sub_device:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default:
|
||||||
|
device_name: null
|
||||||
|
handles: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 尝试打开HPLC子设备的函数。用于初始化和连接泵、检测器、进样器等各个子模块,建立设备通信并进行自检。该函数提供连接验证和错误恢复机制,确保子设备正常启动并准备就绪。适用于设备初始化、系统启动、设备重连等需要建立设备连接的场景。
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
device_name:
|
||||||
|
type: string
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: try_open_sub_device参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
execute_command_from_outer:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
command: command
|
||||||
|
goal_default:
|
||||||
|
command: ''
|
||||||
|
handles: {}
|
||||||
|
result:
|
||||||
|
success: success
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback:
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- status
|
||||||
|
title: SendCmd_Feedback
|
||||||
|
type: object
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
command:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- command
|
||||||
|
title: SendCmd_Goal
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
type: string
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
|
title: SendCmd_Result
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: SendCmd
|
||||||
|
type: object
|
||||||
|
type: SendCmd
|
||||||
|
module: unilabos.devices.hplc.AgilentHPLC:HPLCDriver
|
||||||
|
status_types:
|
||||||
|
could_run: bool
|
||||||
|
data_file: list
|
||||||
|
device_status: str
|
||||||
|
driver_init_ok: bool
|
||||||
|
finish_status: str
|
||||||
|
is_running: bool
|
||||||
|
status_text: str
|
||||||
|
success: bool
|
||||||
|
type: python
|
||||||
|
config_info: []
|
||||||
|
description: 安捷伦高效液相色谱(HPLC)分析设备,用于复杂化合物的分离、检测和定量分析。该设备通过UI自动化技术控制安捷伦ChemStation软件,实现全自动的样品分析流程。具备序列启动、设备状态监控、数据文件提取、结果处理等功能。支持多样品批量处理和实时状态反馈,适用于药物分析、环境检测、食品安全、化学研究等需要高精度色谱分析的实验室应用。
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema:
|
||||||
|
config:
|
||||||
|
properties:
|
||||||
|
driver_debug:
|
||||||
|
default: false
|
||||||
|
type: string
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
data:
|
||||||
|
properties:
|
||||||
|
could_run:
|
||||||
|
type: boolean
|
||||||
|
data_file:
|
||||||
|
type: array
|
||||||
|
device_status:
|
||||||
|
type: string
|
||||||
|
driver_init_ok:
|
||||||
|
type: boolean
|
||||||
|
finish_status:
|
||||||
|
type: string
|
||||||
|
is_running:
|
||||||
|
type: boolean
|
||||||
|
status_text:
|
||||||
|
type: string
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- status_text
|
||||||
|
- device_status
|
||||||
|
- could_run
|
||||||
|
- driver_init_ok
|
||||||
|
- is_running
|
||||||
|
- success
|
||||||
|
- finish_status
|
||||||
|
- data_file
|
||||||
|
type: object
|
||||||
|
version: 1.0.0
|
||||||
|
hplc.agilent-zhida:
|
||||||
|
category:
|
||||||
|
- characterization_chromatic
|
||||||
|
class:
|
||||||
|
action_value_mappings:
|
||||||
|
abort:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
title: EmptyIn_Feedback
|
||||||
|
type: object
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
title: EmptyIn_Goal
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
title: EmptyIn_Result
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: EmptyIn
|
||||||
|
type: object
|
||||||
|
type: EmptyIn
|
||||||
|
auto-close:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: HPLC设备连接关闭函数。安全地断开与智达HPLC设备的TCP socket连接,释放网络资源。该函数确保连接的正确关闭,避免网络资源泄露。通常在设备使用完毕或系统关闭时调用。
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: close参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
auto-connect:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: HPLC设备连接建立函数。与智达HPLC设备建立TCP socket通信连接,配置通信超时参数。该函数是设备使用前的必要步骤,建立成功后可进行状态查询、方法获取、任务启动等操作。连接失败时会抛出异常。
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: connect参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
get_methods:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
title: EmptyIn_Feedback
|
||||||
|
type: object
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
title: EmptyIn_Goal
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
title: EmptyIn_Result
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: EmptyIn
|
||||||
|
type: object
|
||||||
|
type: EmptyIn
|
||||||
|
start:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
string: string
|
||||||
|
goal_default:
|
||||||
|
string: ''
|
||||||
|
handles: {}
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
title: StrSingleInput_Feedback
|
||||||
|
type: object
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
string:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- string
|
||||||
|
title: StrSingleInput_Goal
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
type: string
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
- success
|
||||||
|
title: StrSingleInput_Result
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: StrSingleInput
|
||||||
|
type: object
|
||||||
|
type: StrSingleInput
|
||||||
|
module: unilabos.devices.zhida_hplc.zhida:ZhidaClient
|
||||||
|
status_types:
|
||||||
|
methods: dict
|
||||||
|
status: dict
|
||||||
|
type: python
|
||||||
|
config_info: []
|
||||||
|
description: 智达高效液相色谱(HPLC)分析设备,用于实验室样品的分离、检测和定量分析。该设备通过TCP socket与HPLC控制系统通信,支持远程控制和状态监控。具备自动进样、梯度洗脱、多检测器数据采集等功能,可执行复杂的色谱分析方法。适用于化学分析、药物检测、环境监测、生物样品分析等需要高精度分离分析的实验室应用场景。
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema:
|
||||||
|
config:
|
||||||
|
properties:
|
||||||
|
host:
|
||||||
|
default: 192.168.1.47
|
||||||
|
type: string
|
||||||
|
port:
|
||||||
|
default: 5792
|
||||||
|
type: string
|
||||||
|
timeout:
|
||||||
|
default: 10.0
|
||||||
|
type: string
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
data:
|
||||||
|
properties:
|
||||||
|
methods:
|
||||||
|
type: object
|
||||||
|
status:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- status
|
||||||
|
- methods
|
||||||
|
type: object
|
||||||
|
version: 1.0.0
|
||||||
@@ -1,225 +1,4 @@
|
|||||||
hplc.agilent:
|
raman.home_made:
|
||||||
category:
|
|
||||||
- characterization_optic
|
|
||||||
class:
|
|
||||||
action_value_mappings:
|
|
||||||
auto-check_status:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default: {}
|
|
||||||
handles: []
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: 检查安捷伦HPLC设备状态的函数。用于监控设备的运行状态、连接状态、错误信息等关键指标。该函数定期查询设备状态,确保系统稳定运行,及时发现和报告设备异常。适用于自动化流程中的设备监控、故障诊断、系统维护等场景。
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: check_status参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-extract_data_from_txt:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
file_path: null
|
|
||||||
handles: []
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: 从文本文件中提取分析数据的函数。用于解析安捷伦HPLC生成的结果文件,提取峰面积、保留时间、浓度等关键分析数据。支持多种文件格式的自动识别和数据结构化处理,为后续数据分析和报告生成提供标准化的数据格式。适用于批量数据处理、结果验证、质量控制等分析工作流程。
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
file_path:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- file_path
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: extract_data_from_txt参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-start_sequence:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
params: null
|
|
||||||
resource: null
|
|
||||||
wf_name: null
|
|
||||||
handles: []
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: 启动安捷伦HPLC分析序列的函数。用于执行预定义的分析方法序列,包括样品进样、色谱分离、检测等完整的分析流程。支持参数配置、资源分配、工作流程管理等功能,实现全自动的样品分析。适用于批量样品处理、标准化分析、质量检测等需要连续自动分析的应用场景。
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
params:
|
|
||||||
type: string
|
|
||||||
resource:
|
|
||||||
type: object
|
|
||||||
wf_name:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- wf_name
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: start_sequence参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-try_close_sub_device:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
device_name: null
|
|
||||||
handles: []
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: 尝试关闭HPLC子设备的函数。用于安全地关闭泵、检测器、进样器等各个子模块,确保设备正常断开连接并保护硬件安全。该函数提供错误处理和状态确认机制,避免强制关闭可能造成的设备损坏。适用于设备维护、系统重启、紧急停机等需要安全关闭设备的场景。
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
device_name:
|
|
||||||
type: string
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: try_close_sub_device参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-try_open_sub_device:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
device_name: null
|
|
||||||
handles: []
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: 尝试打开HPLC子设备的函数。用于初始化和连接泵、检测器、进样器等各个子模块,建立设备通信并进行自检。该函数提供连接验证和错误恢复机制,确保子设备正常启动并准备就绪。适用于设备初始化、系统启动、设备重连等需要建立设备连接的场景。
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
device_name:
|
|
||||||
type: string
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: try_open_sub_device参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
execute_command_from_outer:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
command: command
|
|
||||||
goal_default:
|
|
||||||
command: ''
|
|
||||||
handles: []
|
|
||||||
result:
|
|
||||||
success: success
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback:
|
|
||||||
properties:
|
|
||||||
status:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- status
|
|
||||||
title: SendCmd_Feedback
|
|
||||||
type: object
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
command:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- command
|
|
||||||
title: SendCmd_Goal
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
properties:
|
|
||||||
return_info:
|
|
||||||
type: string
|
|
||||||
success:
|
|
||||||
type: boolean
|
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
- success
|
|
||||||
title: SendCmd_Result
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: SendCmd
|
|
||||||
type: object
|
|
||||||
type: SendCmd
|
|
||||||
module: unilabos.devices.hplc.AgilentHPLC:HPLCDriver
|
|
||||||
status_types:
|
|
||||||
could_run: bool
|
|
||||||
data_file: list
|
|
||||||
device_status: str
|
|
||||||
driver_init_ok: bool
|
|
||||||
finish_status: str
|
|
||||||
is_running: bool
|
|
||||||
status_text: str
|
|
||||||
success: bool
|
|
||||||
type: python
|
|
||||||
config_info: []
|
|
||||||
description: 安捷伦高效液相色谱(HPLC)分析设备,用于复杂化合物的分离、检测和定量分析。该设备通过UI自动化技术控制安捷伦ChemStation软件,实现全自动的样品分析流程。具备序列启动、设备状态监控、数据文件提取、结果处理等功能。支持多样品批量处理和实时状态反馈,适用于药物分析、环境检测、食品安全、化学研究等需要高精度色谱分析的实验室应用。
|
|
||||||
handles: []
|
|
||||||
icon: ''
|
|
||||||
init_param_schema:
|
|
||||||
config:
|
|
||||||
properties:
|
|
||||||
driver_debug:
|
|
||||||
default: false
|
|
||||||
type: string
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
data:
|
|
||||||
properties:
|
|
||||||
could_run:
|
|
||||||
type: boolean
|
|
||||||
data_file:
|
|
||||||
type: array
|
|
||||||
device_status:
|
|
||||||
type: string
|
|
||||||
driver_init_ok:
|
|
||||||
type: boolean
|
|
||||||
finish_status:
|
|
||||||
type: string
|
|
||||||
is_running:
|
|
||||||
type: boolean
|
|
||||||
status_text:
|
|
||||||
type: string
|
|
||||||
success:
|
|
||||||
type: boolean
|
|
||||||
required:
|
|
||||||
- status_text
|
|
||||||
- device_status
|
|
||||||
- could_run
|
|
||||||
- driver_init_ok
|
|
||||||
- is_running
|
|
||||||
- success
|
|
||||||
- finish_status
|
|
||||||
- data_file
|
|
||||||
type: object
|
|
||||||
version: 1.0.0
|
|
||||||
raman_home_made:
|
|
||||||
category:
|
category:
|
||||||
- characterization_optic
|
- characterization_optic
|
||||||
class:
|
class:
|
||||||
@@ -229,7 +8,7 @@ raman_home_made:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default:
|
goal_default:
|
||||||
int_time: null
|
int_time: null
|
||||||
handles: []
|
handles: {}
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 设置CCD检测器积分时间的函数。用于配置拉曼光谱仪的信号采集时间,控制光谱数据的质量和信噪比。较长的积分时间可获得更高的信号强度和更好的光谱质量,但会增加测量时间。该函数允许根据样品特性和测量要求动态调整检测参数,优化测量效果。
|
description: 设置CCD检测器积分时间的函数。用于配置拉曼光谱仪的信号采集时间,控制光谱数据的质量和信噪比。较长的积分时间可获得更高的信号强度和更好的光谱质量,但会增加测量时间。该函数允许根据样品特性和测量要求动态调整检测参数,优化测量效果。
|
||||||
@@ -253,7 +32,7 @@ raman_home_made:
|
|||||||
goal: {}
|
goal: {}
|
||||||
goal_default:
|
goal_default:
|
||||||
output_voltage_laser: null
|
output_voltage_laser: null
|
||||||
handles: []
|
handles: {}
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 设置激光器输出功率的函数。用于控制拉曼光谱仪激光器的功率输出,调节激光强度以适应不同样品的测量需求。适当的激光功率能够获得良好的拉曼信号同时避免样品损伤。该函数支持精确的功率控制,确保测量结果的稳定性和重现性。
|
description: 设置激光器输出功率的函数。用于控制拉曼光谱仪激光器的功率输出,调节激光强度以适应不同样品的测量需求。适当的激光功率能够获得良好的拉曼信号同时避免样品损伤。该函数支持精确的功率控制,确保测量结果的稳定性和重现性。
|
||||||
@@ -278,7 +57,7 @@ raman_home_made:
|
|||||||
goal_default:
|
goal_default:
|
||||||
int_time: null
|
int_time: null
|
||||||
laser_power: null
|
laser_power: null
|
||||||
handles: []
|
handles: {}
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 执行无背景扣除的拉曼光谱测量函数。用于直接采集样品的拉曼光谱信号,不进行背景校正处理。该函数配置积分时间和激光功率参数,获取原始光谱数据用于后续的数据处理分析。适用于对光谱数据质量要求较高或需要自定义背景处理流程的测量场景。
|
description: 执行无背景扣除的拉曼光谱测量函数。用于直接采集样品的拉曼光谱信号,不进行背景校正处理。该函数配置积分时间和激光功率参数,获取原始光谱数据用于后续的数据处理分析。适用于对光谱数据质量要求较高或需要自定义背景处理流程的测量场景。
|
||||||
@@ -308,7 +87,7 @@ raman_home_made:
|
|||||||
int_time: null
|
int_time: null
|
||||||
laser_power: null
|
laser_power: null
|
||||||
sample_name: null
|
sample_name: null
|
||||||
handles: []
|
handles: {}
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 执行多次平均的无背景拉曼光谱测量函数。通过多次测量取平均值来提高光谱数据的信噪比和测量精度,减少随机噪声影响。该函数支持自定义平均次数、积分时间、激光功率等参数,并可为样品指定名称便于数据管理。适用于对测量精度要求较高的定量分析和研究应用。
|
description: 执行多次平均的无背景拉曼光谱测量函数。通过多次测量取平均值来提高光谱数据的信噪比和测量精度,减少随机噪声影响。该函数支持自定义平均次数、积分时间、激光功率等参数,并可为样品指定名称便于数据管理。适用于对测量精度要求较高的定量分析和研究应用。
|
||||||
@@ -342,7 +121,7 @@ raman_home_made:
|
|||||||
command: command
|
command: command
|
||||||
goal_default:
|
goal_default:
|
||||||
command: ''
|
command: ''
|
||||||
handles: []
|
handles: {}
|
||||||
result:
|
result:
|
||||||
success: success
|
success: success
|
||||||
schema:
|
schema:
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user