mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-17 13:01:12 +00:00
@@ -67,14 +67,6 @@ class WSConfig:
|
||||
max_reconnect_attempts = 999 # 最大重连次数
|
||||
ping_interval = 30 # ping间隔(秒)
|
||||
|
||||
# OSS上传配置
|
||||
class OSSUploadConfig:
|
||||
api_host = "" # API主机地址
|
||||
authorization = "" # 授权信息
|
||||
init_endpoint = "" # 初始化端点
|
||||
complete_endpoint = "" # 完成端点
|
||||
max_retries = 3 # 最大重试次数
|
||||
|
||||
# HTTP配置
|
||||
class HTTPConfig:
|
||||
remote_addr = "https://uni-lab.bohrium.com/api/v1" # 远程服务器地址
|
||||
@@ -294,19 +286,7 @@ HTTP 客户端配置用于与云端服务通信:
|
||||
- UAT 环境:`https://uni-lab.uat.bohrium.com/api/v1`
|
||||
- 本地环境:`http://127.0.0.1:48197/api/v1`
|
||||
|
||||
### 4. OSSUploadConfig - OSS 上传配置
|
||||
|
||||
对象存储服务配置,用于文件上传功能:
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
| ------------------- | ---- | ------ | -------------------- |
|
||||
| `api_host` | str | `""` | OSS API 主机地址 |
|
||||
| `authorization` | str | `""` | 授权认证信息 |
|
||||
| `init_endpoint` | str | `""` | 上传初始化端点 |
|
||||
| `complete_endpoint` | str | `""` | 上传完成端点 |
|
||||
| `max_retries` | int | `3` | 上传失败最大重试次数 |
|
||||
|
||||
### 5. ROSConfig - ROS 配置
|
||||
### 4. ROSConfig - ROS 配置
|
||||
|
||||
配置 ROS 消息转换器需要加载的模块:
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
## 概述
|
||||
|
||||
注册表(Registry)是 Uni-Lab 的设备配置系统,采用 YAML 格式定义设备的:
|
||||
|
||||
- 可用动作(Actions)
|
||||
- 状态类型(Status Types)
|
||||
- 初始化参数(Init Parameters)
|
||||
@@ -33,7 +34,7 @@
|
||||
### 核心字段说明
|
||||
|
||||
| 字段名 | 类型 | 需要手写 | 说明 |
|
||||
| ----------------- | ------ | -------- | ----------------------------------- |
|
||||
| ----------------- | ------ | -------- | --------------------------------- |
|
||||
| 设备标识符 | string | 是 | 设备的唯一名字,如 `mock_chiller` |
|
||||
| class | object | 部分 | 设备的核心信息,必须配置 |
|
||||
| description | string | 否 | 设备描述,系统默认给空字符串 |
|
||||
@@ -74,8 +75,8 @@ my_device:
|
||||
goal: { ... }
|
||||
result: { ... }
|
||||
|
||||
description: "设备描述"
|
||||
version: "1.0.0"
|
||||
description: '设备描述'
|
||||
version: '1.0.0'
|
||||
category:
|
||||
- device_category
|
||||
handles: []
|
||||
@@ -106,6 +107,7 @@ my_device:
|
||||
适合大多数场景,快速高效。
|
||||
|
||||
**步骤**:
|
||||
|
||||
1. 启动 Uni-Lab
|
||||
2. 访问 Web 界面的"注册表编辑器"
|
||||
3. 上传您的 Python 设备驱动文件
|
||||
@@ -125,6 +127,7 @@ unilab -g dev.json --complete_registry --registry_path ./my_registry
|
||||
```
|
||||
|
||||
系统会:
|
||||
|
||||
1. 扫描 Python 类
|
||||
2. 分析方法签名和类型
|
||||
3. 自动生成缺失的字段
|
||||
@@ -186,6 +189,7 @@ my_device:
|
||||
| ROS 动作类型 | 标准 ROS 动作 | goal_default 和 schema |
|
||||
|
||||
**常用的 ROS 动作类型**:
|
||||
|
||||
- `SendCmd`:发送简单命令
|
||||
- `NavigateThroughPoses`:导航动作
|
||||
- `SingleJointPosition`:单关节位置控制
|
||||
@@ -297,7 +301,7 @@ my_device:
|
||||
### 识别规则
|
||||
|
||||
| Python 类型 | placeholder_keys 值 | 前端效果 |
|
||||
|-----------|-------------------|---------|
|
||||
| -------------------- | -------------------- | -------------- |
|
||||
| `ResourceSlot` | `unilabos_resources` | 单选资源下拉框 |
|
||||
| `List[ResourceSlot]` | `unilabos_resources` | 多选资源下拉框 |
|
||||
| `DeviceSlot` | `unilabos_devices` | 单选设备下拉框 |
|
||||
@@ -313,6 +317,7 @@ placeholder_keys:
|
||||
```
|
||||
|
||||
**前端渲染**:
|
||||
|
||||
```
|
||||
Source: [下拉选择框 ▼]
|
||||
├── plate_1 (96孔板)
|
||||
@@ -329,6 +334,7 @@ placeholder_keys:
|
||||
```
|
||||
|
||||
**前端渲染**:
|
||||
|
||||
```
|
||||
Targets: [多选下拉框 ▼]
|
||||
☑ plate_1 (96孔板)
|
||||
@@ -345,6 +351,7 @@ placeholder_keys:
|
||||
```
|
||||
|
||||
**前端渲染**:
|
||||
|
||||
```
|
||||
Pump: [下拉选择框 ▼]
|
||||
├── pump_1 (注射泵A)
|
||||
@@ -360,6 +367,7 @@ placeholder_keys:
|
||||
```
|
||||
|
||||
**前端渲染**:
|
||||
|
||||
```
|
||||
Sync Devices: [多选下拉框 ▼]
|
||||
☑ heater_1 (加热器A)
|
||||
@@ -414,7 +422,7 @@ placeholder_keys:
|
||||
|
||||
### status_types
|
||||
|
||||
系统会扫描你的 Python 类,从状态方法(property或get_方法)自动生成这部分:
|
||||
系统会扫描你的 Python 类,从状态方法(property 或 get\_方法)自动生成这部分:
|
||||
|
||||
```yaml
|
||||
status_types:
|
||||
@@ -424,6 +432,7 @@ status_types:
|
||||
```
|
||||
|
||||
**注意事项**:
|
||||
|
||||
- 系统会查找所有 `get_` 开头的方法和 `@property` 装饰的属性
|
||||
- 类型会自动转成相应的类型(如 `str`、`float`、`bool`)
|
||||
- 如果类型是 `Any`、`None` 或未知的,默认使用 `String`
|
||||
@@ -459,6 +468,7 @@ init_param_schema:
|
||||
```
|
||||
|
||||
**生成规则**:
|
||||
|
||||
- `config` 部分:分析 `__init__` 方法的参数、类型和默认值
|
||||
- `data` 部分:根据 `status_types` 生成前端显示用的类型定义
|
||||
|
||||
@@ -619,22 +629,22 @@ advanced_liquid_handler:
|
||||
result:
|
||||
success: success
|
||||
schema:
|
||||
description: "转移液体"
|
||||
description: '转移液体'
|
||||
properties:
|
||||
goal:
|
||||
properties:
|
||||
source:
|
||||
type: object
|
||||
description: "源容器"
|
||||
description: '源容器'
|
||||
target:
|
||||
type: object
|
||||
description: "目标容器"
|
||||
description: '目标容器'
|
||||
volume:
|
||||
type: number
|
||||
description: "体积(μL)"
|
||||
description: '体积(μL)'
|
||||
tip:
|
||||
type: object
|
||||
description: "枪头(可选)"
|
||||
description: '枪头(可选)'
|
||||
required:
|
||||
- source
|
||||
- target
|
||||
@@ -668,12 +678,12 @@ advanced_liquid_handler:
|
||||
result:
|
||||
success: success
|
||||
|
||||
description: "高级液体处理工作站,支持多目标转移和设备协同"
|
||||
version: "1.0.0"
|
||||
description: '高级液体处理工作站,支持多目标转移和设备协同'
|
||||
version: '1.0.0'
|
||||
category:
|
||||
- liquid_handling
|
||||
handles: []
|
||||
icon: ""
|
||||
icon: ''
|
||||
```
|
||||
|
||||
### 另一个完整示例:温度控制器
|
||||
@@ -895,6 +905,7 @@ cat unilabos/registry/devices/my_device.yaml
|
||||
### 2. 验证 placeholder_keys
|
||||
|
||||
确认:
|
||||
|
||||
- ResourceSlot 参数有 `unilabos_resources`
|
||||
- DeviceSlot 参数有 `unilabos_devices`
|
||||
- List 类型被正确识别
|
||||
@@ -919,8 +930,10 @@ python -c "from unilabos.devices.my_module.my_device import MyDevice"
|
||||
### Q1: placeholder_keys 没有自动生成
|
||||
|
||||
**检查**:
|
||||
|
||||
1. 是否使用了`--complete_registry`参数?
|
||||
2. 类型注解是否正确?
|
||||
|
||||
```python
|
||||
# ✓ 正确
|
||||
def method(self, resource: ResourceSlot):
|
||||
@@ -928,6 +941,7 @@ python -c "from unilabos.devices.my_module.my_device import MyDevice"
|
||||
# ✗ 错误(缺少类型注解)
|
||||
def method(self, resource):
|
||||
```
|
||||
|
||||
3. 是否正确导入?
|
||||
```python
|
||||
from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot
|
||||
@@ -938,6 +952,7 @@ python -c "from unilabos.devices.my_module.my_device import MyDevice"
|
||||
**原因**: placeholder_keys 未正确配置
|
||||
|
||||
**解决**:
|
||||
|
||||
```yaml
|
||||
# 检查YAML中是否有
|
||||
placeholder_keys:
|
||||
@@ -947,6 +962,7 @@ placeholder_keys:
|
||||
### Q3: 多选不工作
|
||||
|
||||
**检查类型注解**:
|
||||
|
||||
```python
|
||||
# ✓ 正确 - 会生成多选
|
||||
def method(self, resources: List[ResourceSlot]):
|
||||
@@ -960,6 +976,7 @@ def method(self, resources: ResourceSlot):
|
||||
**说明**: 运行时会自动转换
|
||||
|
||||
前端传递:
|
||||
|
||||
```json
|
||||
{
|
||||
"resource": "plate_1" // 字符串ID
|
||||
@@ -967,6 +984,7 @@ def method(self, resources: ResourceSlot):
|
||||
```
|
||||
|
||||
运行时收到:
|
||||
|
||||
```python
|
||||
resource.id # "plate_1"
|
||||
resource.name # "96孔板"
|
||||
@@ -977,6 +995,7 @@ resource.type # "resource"
|
||||
### Q5: 设备加载不了
|
||||
|
||||
**检查**:
|
||||
|
||||
1. 确认 `class.module` 路径是否正确
|
||||
2. 确认 Python 驱动类能否正常导入
|
||||
3. 使用 yaml 验证器检查文件格式
|
||||
@@ -985,6 +1004,7 @@ resource.type # "resource"
|
||||
### Q6: 自动生成失败
|
||||
|
||||
**检查**:
|
||||
|
||||
1. 确认类继承了正确的基类
|
||||
2. 确保状态方法的返回类型注解清晰
|
||||
3. 检查类能否被动态导入
|
||||
@@ -993,6 +1013,7 @@ resource.type # "resource"
|
||||
### Q7: 前端显示问题
|
||||
|
||||
**解决步骤**:
|
||||
|
||||
1. 删除旧的 yaml 文件,用编辑器重新生成
|
||||
2. 清除浏览器缓存,重新加载页面
|
||||
3. 确认必需字段(如 `schema`)都存在
|
||||
@@ -1001,6 +1022,7 @@ resource.type # "resource"
|
||||
### Q8: 动作执行出错
|
||||
|
||||
**检查**:
|
||||
|
||||
1. 确认动作方法名符合规范(如 `execute_<action_name>`)
|
||||
2. 检查 `goal` 字段的参数映射是否正确
|
||||
3. 确认方法返回值格式符合 `result` 映射
|
||||
@@ -1075,6 +1097,7 @@ def method(
|
||||
```
|
||||
|
||||
5. **方法命名规范**
|
||||
|
||||
- 状态方法使用 `@property` 装饰器或 `get_` 前缀
|
||||
- 动作方法使用动词开头
|
||||
- 保持命名清晰、一致
|
||||
@@ -1114,5 +1137,3 @@ def method(
|
||||
- Python [typing 模块](https://docs.python.org/3/library/typing.html)
|
||||
- [YAML 语法](https://yaml.org/)
|
||||
- [JSON Schema](https://json-schema.org/)
|
||||
|
||||
|
||||
|
||||
@@ -50,8 +50,6 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||
self.client = tcp.register_node_list(self.nodes)
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 2. 编写驱动与寄存器读写
|
||||
|
||||
### 2.1 寄存器示例
|
||||
@@ -95,8 +93,8 @@ def start_and_read_metrics(self):
|
||||
|
||||
完成工站类与驱动后,需要生成(或更新)工站注册表供系统识别。
|
||||
|
||||
|
||||
### 3.1 新增工站设备(或资源)首次生成注册表
|
||||
|
||||
首先通过以下命令启动 unilab。进入 unilab 系统状态检查页面
|
||||
|
||||
```bash
|
||||
@@ -112,6 +110,7 @@ python unilabos\app\main.py -g celljson.json --ak <user的AK> --sk <user的SK>
|
||||

|
||||
|
||||
步骤说明:
|
||||
|
||||
1. 选择新增的工站`coin_cell_assembly.py`文件
|
||||
2. 点击分析按钮,分析`coin_cell_assembly.py`文件
|
||||
3. 选择`coin_cell_assembly.py`文件中继承`WorkstationBase`类
|
||||
@@ -124,20 +123,16 @@ python unilabos\app\main.py -g celljson.json --ak <user的AK> --sk <user的SK>
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 3.2 添加新生成注册表
|
||||
|
||||
在`unilabos\registry\devices`目录下新建一个 yaml 文件,此处新建文件命名为`coincellassemblyworkstation_device.yaml`,将上面生成的新的注册表信息粘贴到`coincellassemblyworkstation_device.yaml`文件中。
|
||||
|
||||
在终端输入以下命令进行注册表补全操作。
|
||||
|
||||
```bash
|
||||
python unilabos\app\register.py --complete_registry
|
||||
```
|
||||
|
||||
|
||||
### 3.3 启动并上传注册表
|
||||
|
||||
新增设备之后,启动 unilab 需要增加`--upload_registry`参数,来上传注册表信息。
|
||||
@@ -159,6 +154,7 @@ module: unilabos.devices.workstation.coin_cell_assembly.coin_cell_assembly:CoinC
|
||||
### 4.2 首次接入流程
|
||||
|
||||
首次新增设备(或资源)需要完整流程:
|
||||
|
||||
1. ✅ 在网页端生成注册表信息
|
||||
2. ✅ 使用 `--complete_registry` 补全注册表
|
||||
3. ✅ 使用 `--upload_registry` 上传注册表信息
|
||||
@@ -166,6 +162,7 @@ module: unilabos.devices.workstation.coin_cell_assembly.coin_cell_assembly:CoinC
|
||||
### 4.3 驱动更新流程
|
||||
|
||||
如果不是新增设备,仅修改了工站驱动的 `.py` 文件:
|
||||
|
||||
1. ✅ 运行 `--complete_registry` 补全注册表
|
||||
2. ✅ 运行 `--upload_registry` 上传注册表
|
||||
3. ❌ 不需要在网页端重新生成注册表
|
||||
@@ -203,5 +200,3 @@ module: unilabos.devices.workstation.coin_cell_assembly.coin_cell_assembly:CoinC
|
||||
5. ✅ 新增设备与更新驱动的区别
|
||||
|
||||
这个案例展示了完整的 PLC 设备接入流程,可以作为其他类似设备接入的参考模板。
|
||||
|
||||
|
||||
|
||||
@@ -592,7 +592,7 @@ class PLCWorkstation(WorkstationBase):
|
||||
### 8.1 WorkstationBase 核心属性
|
||||
|
||||
| 属性 | 类型 | 说明 |
|
||||
| --------------------------- | ----------------------- | ----------------------------- |
|
||||
| ------------------------- | ----------------------- | ------------------------------- |
|
||||
| `_ros_node` | ROS2WorkstationNode | ROS 节点引用,由 post_init 设置 |
|
||||
| `deck` | Deck | PyLabRobot Deck,本地物料系统 |
|
||||
| `plr_resources` | Dict[str, PLRResource] | 物料资源映射 |
|
||||
|
||||
@@ -592,4 +592,3 @@ ros2 topic list
|
||||
- [ROS2 网络配置](https://docs.ros.org/en/humble/Tutorials/Advanced/Networking.html)
|
||||
- [DDS 配置](https://fast-dds.docs.eprosima.com/)
|
||||
- Uni-Lab 云平台文档
|
||||
|
||||
|
||||
@@ -1,161 +1,156 @@
|
||||
import argparse
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
import requests
|
||||
|
||||
from unilabos.config.config import OSSUploadConfig
|
||||
from unilabos.app.web.client import http_client, HTTPClient
|
||||
from unilabos.utils import logger
|
||||
|
||||
|
||||
def _init_upload(file_path: str, oss_path: str, filename: Optional[str] = None,
|
||||
process_key: str = "file-upload", device_id: str = "default",
|
||||
expires_hours: int = 1) -> Tuple[bool, Dict]:
|
||||
def _get_oss_token(
|
||||
filename: str,
|
||||
driver_name: str = "default",
|
||||
exp_type: str = "default",
|
||||
client: Optional[HTTPClient] = None,
|
||||
) -> Tuple[bool, Dict]:
|
||||
"""
|
||||
初始化上传过程
|
||||
获取OSS上传Token
|
||||
|
||||
Args:
|
||||
file_path: 本地文件路径
|
||||
oss_path: OSS目标路径
|
||||
filename: 文件名,如果为None则使用file_path的文件名
|
||||
process_key: 处理键
|
||||
device_id: 设备ID
|
||||
expires_hours: 链接过期小时数
|
||||
filename: 文件名
|
||||
driver_name: 驱动名称
|
||||
exp_type: 实验类型
|
||||
client: HTTPClient实例,如果不提供则使用默认的http_client
|
||||
|
||||
Returns:
|
||||
(成功标志, 响应数据)
|
||||
(成功标志, Token数据字典包含token/path/host/expires)
|
||||
"""
|
||||
if filename is None:
|
||||
filename = os.path.basename(file_path)
|
||||
# 使用提供的client或默认的http_client
|
||||
if client is None:
|
||||
client = http_client
|
||||
|
||||
# 构造初始化请求
|
||||
url = f"{OSSUploadConfig.api_host}{OSSUploadConfig.init_endpoint}"
|
||||
headers = {
|
||||
"Authorization": OSSUploadConfig.authorization,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
# 构造scene参数: driver_name-exp_type
|
||||
scene = f"{driver_name}-{exp_type}"
|
||||
|
||||
payload = {
|
||||
"device_id": device_id,
|
||||
"process_key": process_key,
|
||||
"filename": filename,
|
||||
"path": oss_path,
|
||||
"expires_hours": expires_hours
|
||||
}
|
||||
# 构造请求URL,使用client的remote_addr(已包含/api/v1/)
|
||||
url = f"{client.remote_addr}/applications/token"
|
||||
params = {"scene": scene, "filename": filename}
|
||||
|
||||
try:
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
if response.status_code == 201:
|
||||
result = response.json()
|
||||
if result.get("code") == "10000":
|
||||
return True, result.get("data", {})
|
||||
logger.info(f"[OSS] 请求预签名URL: scene={scene}, filename={filename}")
|
||||
response = requests.get(url, params=params, headers={"Authorization": f"Lab {client.auth}"}, timeout=10)
|
||||
|
||||
print(f"初始化上传失败: {response.status_code}, {response.text}")
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
if result.get("code") == 0:
|
||||
data = result.get("data", {})
|
||||
|
||||
# 转换expires时间戳为可读格式
|
||||
expires_timestamp = data.get("expires", 0)
|
||||
expires_datetime = datetime.fromtimestamp(expires_timestamp)
|
||||
expires_str = expires_datetime.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
logger.info(f"[OSS] 获取预签名URL成功")
|
||||
logger.info(f"[OSS] - URL: {data.get('url', 'N/A')}")
|
||||
logger.info(f"[OSS] - Expires: {expires_str} (timestamp: {expires_timestamp})")
|
||||
|
||||
return True, data
|
||||
|
||||
logger.error(f"[OSS] 获取预签名URL失败: {response.status_code}, {response.text}")
|
||||
return False, {}
|
||||
except Exception as e:
|
||||
print(f"初始化上传异常: {str(e)}")
|
||||
logger.error(f"[OSS] 获取预签名URL异常: {str(e)}")
|
||||
return False, {}
|
||||
|
||||
|
||||
def _put_upload(file_path: str, upload_url: str) -> bool:
|
||||
"""
|
||||
执行PUT上传
|
||||
使用预签名URL上传文件到OSS
|
||||
|
||||
Args:
|
||||
file_path: 本地文件路径
|
||||
upload_url: 上传URL
|
||||
upload_url: 完整的预签名上传URL
|
||||
|
||||
Returns:
|
||||
是否成功
|
||||
"""
|
||||
try:
|
||||
logger.info(f"[OSS] 开始上传文件: {file_path}")
|
||||
|
||||
with open(file_path, "rb") as f:
|
||||
response = requests.put(upload_url, data=f)
|
||||
# 使用预签名URL上传,不需要额外的认证header
|
||||
response = requests.put(upload_url, data=f, timeout=300)
|
||||
|
||||
if response.status_code == 200:
|
||||
logger.info(f"[OSS] 文件上传成功")
|
||||
return True
|
||||
|
||||
print(f"PUT上传失败: {response.status_code}, {response.text}")
|
||||
logger.error(f"[OSS] 上传失败: {response.status_code}")
|
||||
logger.error(f"[OSS] 响应内容: {response.text[:500] if response.text else '无响应内容'}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"PUT上传异常: {str(e)}")
|
||||
logger.error(f"[OSS] 上传异常: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def _complete_upload(uuid: str) -> bool:
|
||||
"""
|
||||
完成上传过程
|
||||
|
||||
Args:
|
||||
uuid: 上传的UUID
|
||||
|
||||
Returns:
|
||||
是否成功
|
||||
"""
|
||||
url = f"{OSSUploadConfig.api_host}{OSSUploadConfig.complete_endpoint}"
|
||||
headers = {
|
||||
"Authorization": OSSUploadConfig.authorization,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
payload = {
|
||||
"uuid": uuid
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(url, headers=headers, json=payload)
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
if result.get("code") == "10000":
|
||||
return True
|
||||
|
||||
print(f"完成上传失败: {response.status_code}, {response.text}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"完成上传异常: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def oss_upload(file_path: str, oss_path: str, filename: Optional[str] = None,
|
||||
process_key: str = "file-upload", device_id: str = "default") -> bool:
|
||||
def oss_upload(
|
||||
file_path: str,
|
||||
filename: Optional[str] = None,
|
||||
driver_name: str = "default",
|
||||
exp_type: str = "default",
|
||||
max_retries: int = 3,
|
||||
client: Optional[HTTPClient] = None,
|
||||
) -> Dict:
|
||||
"""
|
||||
文件上传主函数,包含重试机制
|
||||
|
||||
Args:
|
||||
file_path: 本地文件路径
|
||||
oss_path: OSS目标路径
|
||||
filename: 文件名,如果为None则使用file_path的文件名
|
||||
process_key: 处理键
|
||||
device_id: 设备ID
|
||||
driver_name: 驱动名称,用于构造scene
|
||||
exp_type: 实验类型,用于构造scene
|
||||
max_retries: 最大重试次数
|
||||
client: HTTPClient实例,如果不提供则使用默认的http_client
|
||||
|
||||
Returns:
|
||||
是否成功上传
|
||||
Dict: {
|
||||
"success": bool, # 是否上传成功
|
||||
"original_path": str, # 原始文件路径
|
||||
"oss_path": str # OSS路径(成功时)或空字符串(失败时)
|
||||
}
|
||||
"""
|
||||
max_retries = OSSUploadConfig.max_retries
|
||||
if filename is None:
|
||||
filename = os.path.basename(file_path)
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
logger.error(f"[OSS] 文件不存在: {file_path}")
|
||||
return {"success": False, "original_path": file_path, "oss_path": ""}
|
||||
|
||||
retry_count = 0
|
||||
oss_path = ""
|
||||
|
||||
while retry_count < max_retries:
|
||||
try:
|
||||
# 步骤1:初始化上传
|
||||
init_success, init_data = _init_upload(
|
||||
file_path=file_path,
|
||||
oss_path=oss_path,
|
||||
filename=filename,
|
||||
process_key=process_key,
|
||||
device_id=device_id
|
||||
# 步骤1:获取预签名URL
|
||||
token_success, token_data = _get_oss_token(
|
||||
filename=filename, driver_name=driver_name, exp_type=exp_type, client=client
|
||||
)
|
||||
|
||||
if not init_success:
|
||||
print(f"初始化上传失败,重试 {retry_count + 1}/{max_retries}")
|
||||
if not token_success:
|
||||
logger.warning(f"[OSS] 获取预签名URL失败,重试 {retry_count + 1}/{max_retries}")
|
||||
retry_count += 1
|
||||
time.sleep(1) # 等待1秒后重试
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
# 获取UUID和上传URL
|
||||
uuid = init_data.get("uuid")
|
||||
upload_url = init_data.get("upload_url")
|
||||
# 获取预签名URL和OSS路径
|
||||
upload_url = token_data.get("url")
|
||||
oss_path = token_data.get("path", "")
|
||||
|
||||
if not uuid or not upload_url:
|
||||
print(f"初始化上传返回数据不完整,重试 {retry_count + 1}/{max_retries}")
|
||||
if not upload_url:
|
||||
logger.warning(f"[OSS] 无法获取上传URL,API未返回url字段")
|
||||
retry_count += 1
|
||||
time.sleep(1)
|
||||
continue
|
||||
@@ -163,69 +158,82 @@ def oss_upload(file_path: str, oss_path: str, filename: Optional[str] = None,
|
||||
# 步骤2:PUT上传文件
|
||||
put_success = _put_upload(file_path, upload_url)
|
||||
if not put_success:
|
||||
print(f"PUT上传失败,重试 {retry_count + 1}/{max_retries}")
|
||||
retry_count += 1
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
# 步骤3:完成上传
|
||||
complete_success = _complete_upload(uuid)
|
||||
if not complete_success:
|
||||
print(f"完成上传失败,重试 {retry_count + 1}/{max_retries}")
|
||||
logger.warning(f"[OSS] PUT上传失败,重试 {retry_count + 1}/{max_retries}")
|
||||
retry_count += 1
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
# 所有步骤都成功
|
||||
print(f"文件 {file_path} 上传成功")
|
||||
return True
|
||||
logger.info(f"[OSS] 文件 {file_path} 上传成功")
|
||||
return {"success": True, "original_path": file_path, "oss_path": oss_path}
|
||||
|
||||
except Exception as e:
|
||||
print(f"上传过程异常: {str(e)},重试 {retry_count + 1}/{max_retries}")
|
||||
logger.error(f"[OSS] 上传过程异常: {str(e)},重试 {retry_count + 1}/{max_retries}")
|
||||
retry_count += 1
|
||||
time.sleep(1)
|
||||
|
||||
print(f"文件 {file_path} 上传失败,已达到最大重试次数 {max_retries}")
|
||||
return False
|
||||
logger.error(f"[OSS] 文件 {file_path} 上传失败,已达到最大重试次数 {max_retries}")
|
||||
return {"success": False, "original_path": file_path, "oss_path": oss_path}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# python -m unilabos.app.oss_upload -f /path/to/your/file.txt
|
||||
# python -m unilabos.app.oss_upload -f /path/to/your/file.txt --driver HPLC --type test
|
||||
# python -m unilabos.app.oss_upload -f /path/to/your/file.txt --driver HPLC --type test \
|
||||
# --ak xxx --sk yyy --remote-addr http://xxx/api/v1
|
||||
# 命令行参数解析
|
||||
parser = argparse.ArgumentParser(description='文件上传测试工具')
|
||||
parser.add_argument('--file', '-f', type=str, required=True, help='要上传的本地文件路径')
|
||||
parser.add_argument('--path', '-p', type=str, default='/HPLC1/Any', help='OSS目标路径')
|
||||
parser.add_argument('--device', '-d', type=str, default='test-device', help='设备ID')
|
||||
parser.add_argument('--process', '-k', type=str, default='HPLC-txt-result', help='处理键')
|
||||
parser = argparse.ArgumentParser(description="文件上传测试工具")
|
||||
parser.add_argument("--file", "-f", type=str, required=True, help="要上传的本地文件路径")
|
||||
parser.add_argument("--driver", "-d", type=str, default="default", help="驱动名称")
|
||||
parser.add_argument("--type", "-t", type=str, default="default", help="实验类型")
|
||||
parser.add_argument("--ak", type=str, help="Access Key,如果提供则覆盖配置")
|
||||
parser.add_argument("--sk", type=str, help="Secret Key,如果提供则覆盖配置")
|
||||
parser.add_argument("--remote-addr", type=str, help="远程服务器地址(包含/api/v1),如果提供则覆盖配置")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 检查文件是否存在
|
||||
if not os.path.exists(args.file):
|
||||
print(f"错误:文件 {args.file} 不存在")
|
||||
logger.error(f"错误:文件 {args.file} 不存在")
|
||||
exit(1)
|
||||
|
||||
print("=" * 50)
|
||||
print(f"开始上传文件: {args.file}")
|
||||
print(f"目标路径: {args.path}")
|
||||
print(f"设备ID: {args.device}")
|
||||
print(f"处理键: {args.process}")
|
||||
print("=" * 50)
|
||||
# 如果提供了ak/sk/remote_addr,创建临时HTTPClient
|
||||
temp_client = None
|
||||
if args.ak and args.sk:
|
||||
import base64
|
||||
|
||||
auth = base64.b64encode(f"{args.ak}:{args.sk}".encode("utf-8")).decode("utf-8")
|
||||
remote_addr = args.remote_addr if args.remote_addr else http_client.remote_addr
|
||||
temp_client = HTTPClient(remote_addr=remote_addr, auth=auth)
|
||||
logger.info(f"[配置] 使用自定义配置: remote_addr={remote_addr}")
|
||||
elif args.remote_addr:
|
||||
temp_client = HTTPClient(remote_addr=args.remote_addr, auth=http_client.auth)
|
||||
logger.info(f"[配置] 使用自定义remote_addr: {args.remote_addr}")
|
||||
else:
|
||||
logger.info(f"[配置] 使用默认配置: remote_addr={http_client.remote_addr}")
|
||||
|
||||
logger.info("=" * 50)
|
||||
logger.info(f"开始上传文件: {args.file}")
|
||||
logger.info(f"驱动名称: {args.driver}")
|
||||
logger.info(f"实验类型: {args.type}")
|
||||
logger.info(f"Scene: {args.driver}-{args.type}")
|
||||
logger.info("=" * 50)
|
||||
|
||||
# 执行上传
|
||||
success = oss_upload(
|
||||
result = oss_upload(
|
||||
file_path=args.file,
|
||||
oss_path=args.path,
|
||||
filename=None, # 使用默认文件名
|
||||
process_key=args.process,
|
||||
device_id=args.device
|
||||
driver_name=args.driver,
|
||||
exp_type=args.type,
|
||||
client=temp_client,
|
||||
)
|
||||
|
||||
# 输出结果
|
||||
if success:
|
||||
print("\n√ 文件上传成功!")
|
||||
if result["success"]:
|
||||
logger.info(f"\n√ 文件上传成功!")
|
||||
logger.info(f"原始路径: {result['original_path']}")
|
||||
logger.info(f"OSS路径: {result['oss_path']}")
|
||||
exit(0)
|
||||
else:
|
||||
print("\n× 文件上传失败!")
|
||||
logger.error(f"\n× 文件上传失败!")
|
||||
logger.error(f"原始路径: {result['original_path']}")
|
||||
exit(1)
|
||||
|
||||
|
||||
@@ -36,15 +36,6 @@ class WSConfig:
|
||||
ping_interval = 30 # ping间隔(秒)
|
||||
|
||||
|
||||
# OSS上传配置
|
||||
class OSSUploadConfig:
|
||||
api_host = ""
|
||||
authorization = ""
|
||||
init_endpoint = ""
|
||||
complete_endpoint = ""
|
||||
max_retries = 3
|
||||
|
||||
|
||||
# HTTP配置
|
||||
class HTTPConfig:
|
||||
remote_addr = "http://127.0.0.1:48197/api/v1"
|
||||
|
||||
@@ -405,9 +405,19 @@ class RunningResultChecker(DriverChecker):
|
||||
for i in range(self.driver._finished, temp):
|
||||
sample_id = self.driver._get_resource_sample_id(self.driver._wf_name, i) # 从0开始计数
|
||||
pdf, txt = self.driver.get_data_file(i + 1)
|
||||
device_id = self.driver.device_id if hasattr(self.driver, "device_id") else "default"
|
||||
oss_upload(pdf, f"hplc/{sample_id}/{os.path.basename(pdf)}", process_key="example", device_id=device_id)
|
||||
oss_upload(txt, f"hplc/{sample_id}/{os.path.basename(txt)}", process_key="HPLC-txt-result", device_id=device_id)
|
||||
# 使用新的OSS上传接口,传入driver_name和exp_type
|
||||
pdf_result = oss_upload(pdf, filename=os.path.basename(pdf), driver_name="HPLC", exp_type="analysis")
|
||||
txt_result = oss_upload(txt, filename=os.path.basename(txt), driver_name="HPLC", exp_type="result")
|
||||
|
||||
if pdf_result["success"]:
|
||||
print(f"PDF上传成功: {pdf_result['oss_path']}")
|
||||
else:
|
||||
print(f"PDF上传失败: {pdf_result['original_path']}")
|
||||
|
||||
if txt_result["success"]:
|
||||
print(f"TXT上传成功: {txt_result['oss_path']}")
|
||||
else:
|
||||
print(f"TXT上传失败: {txt_result['original_path']}")
|
||||
# self.driver.extract_data_from_txt()
|
||||
except Exception as ex:
|
||||
self.driver._finished = 0
|
||||
@@ -456,8 +466,12 @@ if __name__ == "__main__":
|
||||
}
|
||||
sample_id = obj._get_resource_sample_id("test", 0)
|
||||
pdf, txt = obj.get_data_file("1", after_time=datetime(2024, 11, 6, 19, 3, 6))
|
||||
oss_upload(pdf, f"hplc/{sample_id}/{os.path.basename(pdf)}", process_key="example")
|
||||
oss_upload(txt, f"hplc/{sample_id}/{os.path.basename(txt)}", process_key="HPLC-txt-result")
|
||||
# 使用新的OSS上传接口,传入driver_name和exp_type
|
||||
pdf_result = oss_upload(pdf, filename=os.path.basename(pdf), driver_name="HPLC", exp_type="analysis")
|
||||
txt_result = oss_upload(txt, filename=os.path.basename(txt), driver_name="HPLC", exp_type="result")
|
||||
|
||||
print(f"PDF上传结果: {pdf_result}")
|
||||
print(f"TXT上传结果: {txt_result}")
|
||||
# driver = HPLCDriver()
|
||||
# for i in range(10000):
|
||||
# print({k: v for k, v in driver._device_status.items() if isinstance(v, str)})
|
||||
|
||||
Reference in New Issue
Block a user