mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-05-23 01:59:59 +00:00
Compare commits
112 Commits
feat/sampl
...
ba7995270a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba7995270a | ||
|
|
9165c9f421 | ||
|
|
e5462f748e | ||
|
|
1abbcccb53 | ||
|
|
45337271ef | ||
|
|
ec606bac3b | ||
|
|
92828ea44a | ||
|
|
63faa15bf2 | ||
|
|
78bc393847 | ||
|
|
575803d53d | ||
|
|
5767a0d55f | ||
|
|
14d82fe559 | ||
|
|
14081acd59 | ||
|
|
f5108104cb | ||
|
|
9c256238af | ||
|
|
742a96e4c0 | ||
|
|
865dd87556 | ||
|
|
86f1640efb | ||
|
|
99ee27bfc2 | ||
|
|
e8f54d50f9 | ||
|
|
201b1064d7 | ||
|
|
2ebe35e70e | ||
|
|
717f236332 | ||
|
|
79c0815b70 | ||
|
|
f431d61d85 | ||
|
|
3af86a07f2 | ||
|
|
d1713fcca1 | ||
|
|
52b460466d | ||
|
|
7efccbc688 | ||
|
|
dc1de44b19 | ||
|
|
4581ee1eeb | ||
|
|
620cb8435f | ||
|
|
83565038cb | ||
|
|
01d281189a | ||
|
|
db22156d77 | ||
|
|
20342c6484 | ||
|
|
008c355754 | ||
|
|
0895252bc1 | ||
|
|
3e43359460 | ||
|
|
73add2dc06 | ||
|
|
dd21d93151 | ||
|
|
e11c3533c7 | ||
|
|
58997f0654 | ||
|
|
fbfc3e30fb | ||
|
|
ed952e8a44 | ||
|
|
1d1c1367df | ||
|
|
c91b600e90 | ||
|
|
49b3c850f9 | ||
|
|
25c94af755 | ||
|
|
861a012747 | ||
|
|
467f0b1115 | ||
|
|
ee63e95f50 | ||
|
|
dbf5df6e4d | ||
|
|
f10c0343ce | ||
|
|
8b6553bdd9 | ||
|
|
e7a4afd6b5 | ||
|
|
f18f6d82fc | ||
|
|
b7c726635c | ||
|
|
c809912fd3 | ||
|
|
d956b27e9f | ||
|
|
ff1e21fcd8 | ||
|
|
b9d9666003 | ||
|
|
91928a87ac | ||
|
|
d7850b050b | ||
|
|
dff70bd72b | ||
|
|
03e3719b18 | ||
|
|
41a018febc | ||
|
|
d776550a4b | ||
|
|
3d8123849a | ||
|
|
d2f204c5b0 | ||
|
|
d8922884b1 | ||
|
|
427afe83d4 | ||
|
|
23c2e3b2f7 | ||
|
|
59c26265e9 | ||
|
|
4c2adea55a | ||
|
|
0f6264503a | ||
|
|
2c554182d3 | ||
|
|
7505e024f3 | ||
|
|
6d319d91ff | ||
|
|
3155b2f97e | ||
|
|
e5e30a1c7d | ||
|
|
4e82f62327 | ||
|
|
95d3456214 | ||
|
|
38bf95b13c | ||
|
|
f2c0bec02c | ||
|
|
e0394bf414 | ||
|
|
975a56415a | ||
|
|
cadbe87e3f | ||
|
|
b993c1f590 | ||
|
|
e0fae94c10 | ||
|
|
b5cd181ac1 | ||
|
|
5c047beb83 | ||
|
|
b40c087143 | ||
|
|
7f1cc3b2a5 | ||
|
|
3f160c2049 | ||
|
|
a54e7c0f23 | ||
|
|
e5015cd5e0 | ||
|
|
514373c164 | ||
|
|
fcea02585a | ||
|
|
07cf690897 | ||
|
|
cfea27460a | ||
|
|
b7d3e980a9 | ||
|
|
f9ed6cb3fb | ||
|
|
699a0b3ce7 | ||
|
|
cf3a20ae79 | ||
|
|
cdf0652020 | ||
|
|
60073ff139 | ||
|
|
a9053b822f | ||
|
|
d238c2ab8b | ||
|
|
9a7d5c7c82 | ||
|
|
4f7d431c0b | ||
|
|
341a1b537c |
@@ -3,7 +3,7 @@
|
||||
|
||||
package:
|
||||
name: unilabos
|
||||
version: 0.10.17
|
||||
version: 0.11.2
|
||||
|
||||
source:
|
||||
path: ../../unilabos
|
||||
@@ -46,13 +46,15 @@ requirements:
|
||||
- jinja2
|
||||
- requests
|
||||
- uvicorn
|
||||
- opcua # [not osx]
|
||||
- if: not osx
|
||||
then:
|
||||
- opcua
|
||||
- pyserial
|
||||
- pandas
|
||||
- pymodbus
|
||||
- matplotlib
|
||||
- pylibftdi
|
||||
- uni-lab::unilabos-env ==0.10.17
|
||||
- uni-lab::unilabos-env ==0.11.2
|
||||
|
||||
about:
|
||||
repository: https://github.com/deepmodeling/Uni-Lab-OS
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
package:
|
||||
name: unilabos-env
|
||||
version: 0.10.17
|
||||
version: 0.11.2
|
||||
|
||||
build:
|
||||
noarch: generic
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
package:
|
||||
name: unilabos-full
|
||||
version: 0.10.17
|
||||
version: 0.11.2
|
||||
|
||||
build:
|
||||
noarch: generic
|
||||
@@ -11,7 +11,7 @@ build:
|
||||
requirements:
|
||||
run:
|
||||
# Base unilabos package (includes unilabos-env)
|
||||
- uni-lab::unilabos ==0.10.17
|
||||
- uni-lab::unilabos ==0.11.2
|
||||
# Documentation tools
|
||||
- sphinx
|
||||
- sphinx_rtd_theme
|
||||
|
||||
196
.cursor/skills/add-device/SKILL.md
Normal file
196
.cursor/skills/add-device/SKILL.md
Normal file
@@ -0,0 +1,196 @@
|
||||
---
|
||||
name: add-device
|
||||
description: Guide for adding new devices to Uni-Lab-OS (接入新设备). Uses @device decorator + AST auto-scanning instead of manual YAML. Walks through device category, communication protocol, driver creation with decorators, and graph file setup. Use when the user wants to add/integrate a new device, create a device driver, write a device class, or mentions 接入设备/添加设备/设备驱动/物模型.
|
||||
---
|
||||
|
||||
# 添加新设备到 Uni-Lab-OS
|
||||
|
||||
**第一步:** 使用 Read 工具读取 `docs/ai_guides/add_device.md`,获取完整的设备接入指南。
|
||||
|
||||
该指南包含设备类别(物模型)列表、通信协议模板、常见错误检查清单等。搜索 `unilabos/devices/` 获取已有设备的实现参考。
|
||||
|
||||
---
|
||||
|
||||
## 装饰器参考
|
||||
|
||||
### @device — 设备类装饰器
|
||||
|
||||
```python
|
||||
from unilabos.registry.decorators import device
|
||||
|
||||
# 单设备
|
||||
@device(
|
||||
id="my_device.vendor", # 注册表唯一标识(必填)
|
||||
category=["temperature"], # 分类标签列表(必填)
|
||||
description="设备描述", # 设备描述
|
||||
display_name="显示名称", # UI 显示名称(默认用 id)
|
||||
icon="DeviceIcon.webp", # 图标文件名
|
||||
version="1.0.0", # 版本号
|
||||
device_type="python", # "python" 或 "ros2"
|
||||
handles=[...], # 端口列表(InputHandle / OutputHandle)
|
||||
model={...}, # 3D 模型配置
|
||||
hardware_interface=HardwareInterface(...), # 硬件通信接口
|
||||
)
|
||||
|
||||
# 多设备(同一个类注册多个设备 ID,各自有不同的 handles 等配置)
|
||||
@device(
|
||||
ids=["pump.vendor.model_A", "pump.vendor.model_B"],
|
||||
id_meta={
|
||||
"pump.vendor.model_A": {"handles": [...], "description": "型号 A"},
|
||||
"pump.vendor.model_B": {"handles": [...], "description": "型号 B"},
|
||||
},
|
||||
category=["pump_and_valve"],
|
||||
)
|
||||
```
|
||||
|
||||
### @action — 动作方法装饰器
|
||||
|
||||
```python
|
||||
from unilabos.registry.decorators import action
|
||||
|
||||
@action # 无参:注册为 UniLabJsonCommand 动作
|
||||
@action() # 同上
|
||||
@action(description="执行操作") # 带描述
|
||||
@action(
|
||||
action_type=HeatChill, # 指定 ROS Action 消息类型
|
||||
goal={"temperature": "temp"}, # Goal 字段映射
|
||||
feedback={}, # Feedback 字段映射
|
||||
result={}, # Result 字段映射
|
||||
handles=[...], # 动作级别端口
|
||||
goal_default={"temp": 25.0}, # Goal 默认值
|
||||
placeholder_keys={...}, # 参数占位符
|
||||
always_free=True, # 不受排队限制
|
||||
auto_prefix=True, # 强制使用 auto- 前缀
|
||||
parent=True, # 从父类 MRO 获取参数签名
|
||||
)
|
||||
```
|
||||
|
||||
**自动识别规则:**
|
||||
- 带 `@action` 的公开方法 → 注册为动作(方法名即动作名)
|
||||
- **不带 `@action` 的公开方法** → 自动注册为 `auto-{方法名}` 动作
|
||||
- `_` 开头的方法 → 不扫描
|
||||
- `@not_action` 标记的方法 → 排除
|
||||
|
||||
### 参数文档 → JSON Schema 元数据
|
||||
|
||||
在 `__init__` 和 action 方法 docstring 的 `Args:` 小节里,使用以下格式生成入参 schema 的显示信息:
|
||||
|
||||
```python
|
||||
"""
|
||||
Args:
|
||||
param[显示名称]: 参数说明,会写入 JSON Schema 的 description。
|
||||
"""
|
||||
```
|
||||
|
||||
- `param[显示名称]` 的显示名称会写入 goal property 的 `title`。
|
||||
- `:` 后面的说明会写入 goal property 的 `description`。
|
||||
- 如果只写 `param: 参数说明`,`title` 会兜底为字段名,`description` 使用参数说明。
|
||||
- 如果没有写参数文档,生成器也会兜底补齐 `title=<字段名>` 和 `description=""`,但新设备应优先写清楚显示名和说明。
|
||||
|
||||
### @topic_config — 状态属性配置
|
||||
|
||||
```python
|
||||
from unilabos.registry.decorators import topic_config
|
||||
|
||||
@property
|
||||
@topic_config(
|
||||
period=5.0, # 发布周期(秒),默认 5.0
|
||||
print_publish=False, # 是否打印发布日志
|
||||
qos=10, # QoS 深度,默认 10
|
||||
name="custom_name", # 自定义发布名称(默认用属性名)
|
||||
)
|
||||
def temperature(self) -> float:
|
||||
return self.data.get("temperature", 0.0)
|
||||
```
|
||||
|
||||
### 辅助装饰器
|
||||
|
||||
```python
|
||||
from unilabos.registry.decorators import not_action, always_free
|
||||
|
||||
@not_action # 标记为非动作(post_init、辅助方法等)
|
||||
@always_free # 标记为不受排队限制(查询类操作)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 设备模板
|
||||
|
||||
```python
|
||||
import logging
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||
from unilabos.registry.decorators import action, device, not_action, topic_config
|
||||
|
||||
@device(
|
||||
id="my_device",
|
||||
category=["my_category"],
|
||||
description="设备描述",
|
||||
display_name="设备显示名",
|
||||
)
|
||||
class MyDevice:
|
||||
"""设备类说明。"""
|
||||
|
||||
_ros_node: BaseROS2DeviceNode
|
||||
|
||||
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
||||
"""
|
||||
初始化设备。
|
||||
|
||||
Args:
|
||||
device_id[设备ID]: 设备实例 ID,默认使用 my_device。
|
||||
config[设备配置]: 设备启动配置。
|
||||
"""
|
||||
self.device_id = device_id or "my_device"
|
||||
self.config = config or {}
|
||||
self.logger = logging.getLogger(f"MyDevice.{self.device_id}")
|
||||
self.data: Dict[str, Any] = {"status": "Idle"}
|
||||
|
||||
@not_action
|
||||
def post_init(self, ros_node: BaseROS2DeviceNode) -> None:
|
||||
self._ros_node = ros_node
|
||||
|
||||
@action
|
||||
async def initialize(self) -> bool:
|
||||
self.data["status"] = "Ready"
|
||||
return True
|
||||
|
||||
@action
|
||||
async def cleanup(self) -> bool:
|
||||
self.data["status"] = "Offline"
|
||||
return True
|
||||
|
||||
@action(description="执行操作")
|
||||
def my_action(self, param: float = 0.0, name: str = "") -> Dict[str, Any]:
|
||||
"""
|
||||
带 @action 装饰器 → 注册为 'my_action' 动作。
|
||||
|
||||
Args:
|
||||
param[操作数值]: 操作使用的数值参数。
|
||||
name[操作名称]: 操作名称或备注。
|
||||
"""
|
||||
return {"success": True}
|
||||
|
||||
def get_info(self) -> Dict[str, Any]:
|
||||
"""无 @action → 自动注册为 'auto-get_info' 动作"""
|
||||
return {"device_id": self.device_id}
|
||||
|
||||
@property
|
||||
@topic_config()
|
||||
def status(self) -> str:
|
||||
return self.data.get("status", "Idle")
|
||||
|
||||
@property
|
||||
@topic_config(period=2.0)
|
||||
def temperature(self) -> float:
|
||||
return self.data.get("temperature", 0.0)
|
||||
```
|
||||
|
||||
### 要点
|
||||
|
||||
- `_ros_node: BaseROS2DeviceNode` 类型标注放在类体顶部
|
||||
- `__init__` 签名固定为 `(self, device_id=None, config=None, **kwargs)`
|
||||
- `post_init` 用 `@not_action` 标记,参数类型标注为 `BaseROS2DeviceNode`
|
||||
- 运行时状态存储在 `self.data` 字典中
|
||||
- 设备文件放在 `unilabos/devices/<category>/` 目录下
|
||||
351
.cursor/skills/add-resource/SKILL.md
Normal file
351
.cursor/skills/add-resource/SKILL.md
Normal file
@@ -0,0 +1,351 @@
|
||||
---
|
||||
name: add-resource
|
||||
description: Guide for adding new resources (materials, bottles, carriers, decks, warehouses) to Uni-Lab-OS (添加新物料/资源). Uses @resource decorator for AST auto-scanning. Covers Bottle, Carrier, Deck, WareHouse definitions. Use when the user wants to add resources, define materials, create a deck layout, add bottles/carriers/plates, or mentions 物料/资源/resource/bottle/carrier/deck/plate/warehouse.
|
||||
---
|
||||
|
||||
# 添加新物料资源
|
||||
|
||||
Uni-Lab-OS 的资源体系基于 PyLabRobot,通过扩展实现 Bottle、Carrier、WareHouse、Deck 等实验室物料管理。使用 `@resource` 装饰器注册,AST 自动扫描生成注册表条目。
|
||||
|
||||
---
|
||||
|
||||
## 资源类型
|
||||
|
||||
| 类型 | 基类 | 用途 | 示例 |
|
||||
|------|------|------|------|
|
||||
| **Bottle** | `Well` (PyLabRobot) | 单个容器(瓶、小瓶、烧杯、反应器) | 试剂瓶、粉末瓶 |
|
||||
| **BottleCarrier** | `ItemizedCarrier` | 多槽位载架(放多个 Bottle) | 6 位试剂架、枪头盒 |
|
||||
| **WareHouse** | `ItemizedCarrier` | 堆栈/仓库(放多个 Carrier) | 4x4 堆栈 |
|
||||
| **Deck** | `Deck` (PyLabRobot) | 工作站台面(放多个 WareHouse) | 反应站 Deck |
|
||||
|
||||
**层级关系:** `Deck` → `WareHouse` → `BottleCarrier` → `Bottle`
|
||||
|
||||
WareHouse 本质上和 Site 是同一概念 — 都是定义一组固定的放置位(slot),只不过 WareHouse 多嵌套了一层 Deck。两者都需要开发者根据实际物理尺寸自行计算各 slot 的偏移坐标。
|
||||
|
||||
---
|
||||
|
||||
## @resource 装饰器
|
||||
|
||||
```python
|
||||
from unilabos.registry.decorators import resource
|
||||
|
||||
@resource(
|
||||
id="my_resource_id", # 注册表唯一标识(必填)
|
||||
category=["bottles"], # 分类标签列表(必填)
|
||||
description="资源描述",
|
||||
icon="", # 图标
|
||||
version="1.0.0",
|
||||
handles=[...], # 端口列表(InputHandle / OutputHandle)
|
||||
model={...}, # 3D 模型配置
|
||||
class_type="pylabrobot", # "python" / "pylabrobot" / "unilabos"
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 创建规范
|
||||
|
||||
### 命名规则
|
||||
|
||||
1. **`name` 参数作为前缀**:所有工厂函数必须接受 `name: str` 参数,创建子物料时以 `name` 作为前缀,确保实例名在运行时全局唯一
|
||||
2. **Bottle 命名约定**:试剂瓶-Bottle,烧杯-Beaker,烧瓶-Flask,小瓶-Vial
|
||||
3. **函数名 = `@resource(id=...)`**:工厂函数名与注册表 id 保持一致
|
||||
|
||||
### 子物料命名示例
|
||||
|
||||
```python
|
||||
# Carrier 内部的 sites 用 name 前缀
|
||||
for k, v in sites.items():
|
||||
v.name = f"{name}_{v.name}" # "堆栈1左_A01", "堆栈1左_B02" ...
|
||||
|
||||
# Carrier 中放置 Bottle 时用 name 前缀
|
||||
carrier[0] = My_Reagent_Bottle(f"{name}_flask_1") # "堆栈1左_flask_1"
|
||||
carrier[i] = My_Solid_Vial(f"{name}_vial_{ordering[i]}") # "堆栈1左_vial_A1"
|
||||
|
||||
# create_homogeneous_resources 使用 name_prefix
|
||||
sites=create_homogeneous_resources(
|
||||
klass=ResourceHolder,
|
||||
locations=[...],
|
||||
name_prefix=name, # 自动生成 "{name}_0", "{name}_1" ...
|
||||
)
|
||||
|
||||
# Deck setup 中用仓库名称作为 name 传入
|
||||
self.warehouses = {
|
||||
"堆栈1左": my_warehouse_4x4("堆栈1左"), # WareHouse.name = "堆栈1左"
|
||||
"试剂堆栈": my_reagent_stack("试剂堆栈"), # WareHouse.name = "试剂堆栈"
|
||||
}
|
||||
```
|
||||
|
||||
### 其他规范
|
||||
|
||||
- **max_volume 单位为 μL**:500mL = 500000
|
||||
- **尺寸单位为 mm**:`diameter`, `height`, `size_x/y/z`, `dx/dy/dz`
|
||||
- **BottleCarrier 必须设置 `num_items_x/y/z`**:用于前端渲染布局
|
||||
- **Deck 的 `__init__` 必须接受 `setup=False`**:图文件中 `config.setup=true` 触发 `setup()`
|
||||
- **按项目分组文件**:同一工作站的资源放在 `unilabos/resources/<project>/` 下
|
||||
- **`__init__` 必须接受 `serialize()` 输出的所有字段**:`serialize()` 输出会作为 `config` 回传到 `__init__`,因此必须通过显式参数或 `**kwargs` 接受,否则反序列化会报错
|
||||
- **持久化运行时状态用 `serialize_state()`**:通过 `_unilabos_state` 字典存储可变信息(如物料内容、液体量),只存 JSON 可序列化的基本类型
|
||||
|
||||
---
|
||||
|
||||
## 资源模板
|
||||
|
||||
### Bottle
|
||||
|
||||
```python
|
||||
from unilabos.registry.decorators import resource
|
||||
from unilabos.resources.itemized_carrier import Bottle
|
||||
|
||||
|
||||
@resource(id="My_Reagent_Bottle", category=["bottles"], description="我的试剂瓶")
|
||||
def My_Reagent_Bottle(
|
||||
name: str,
|
||||
diameter: float = 70.0,
|
||||
height: float = 120.0,
|
||||
max_volume: float = 500000.0,
|
||||
barcode: str = None,
|
||||
) -> Bottle:
|
||||
return Bottle(
|
||||
name=name,
|
||||
diameter=diameter,
|
||||
height=height,
|
||||
max_volume=max_volume,
|
||||
barcode=barcode,
|
||||
model="My_Reagent_Bottle",
|
||||
)
|
||||
```
|
||||
|
||||
**Bottle 参数:**
|
||||
- `name`: 实例名称(运行时唯一,由上层 Carrier 以前缀方式传入)
|
||||
- `diameter`: 瓶体直径 (mm)
|
||||
- `height`: 瓶体高度 (mm)
|
||||
- `max_volume`: 最大容积(**μL**,500mL = 500000)
|
||||
- `barcode`: 条形码(可选)
|
||||
|
||||
### BottleCarrier
|
||||
|
||||
```python
|
||||
from pylabrobot.resources import ResourceHolder
|
||||
from pylabrobot.resources.carrier import create_ordered_items_2d
|
||||
from unilabos.resources.itemized_carrier import BottleCarrier
|
||||
from unilabos.registry.decorators import resource
|
||||
|
||||
|
||||
@resource(id="My_6SlotCarrier", category=["bottle_carriers"], description="六槽位载架")
|
||||
def My_6SlotCarrier(name: str) -> BottleCarrier:
|
||||
sites = create_ordered_items_2d(
|
||||
klass=ResourceHolder,
|
||||
num_items_x=3, num_items_y=2,
|
||||
dx=10.0, dy=10.0, dz=5.0,
|
||||
item_dx=42.0, item_dy=35.0,
|
||||
size_x=20.0, size_y=20.0, size_z=50.0,
|
||||
)
|
||||
# 子 site 用 name 作为前缀
|
||||
for k, v in sites.items():
|
||||
v.name = f"{name}_{v.name}"
|
||||
|
||||
carrier = BottleCarrier(
|
||||
name=name, size_x=146.0, size_y=80.0, size_z=55.0,
|
||||
sites=sites, model="My_6SlotCarrier",
|
||||
)
|
||||
carrier.num_items_x = 3
|
||||
carrier.num_items_y = 2
|
||||
carrier.num_items_z = 1
|
||||
|
||||
# 放置 Bottle 时用 name 作为前缀
|
||||
ordering = ["A1", "B1", "A2", "B2", "A3", "B3"]
|
||||
for i in range(6):
|
||||
carrier[i] = My_Reagent_Bottle(f"{name}_vial_{ordering[i]}")
|
||||
return carrier
|
||||
```
|
||||
|
||||
### WareHouse / Deck 放置位
|
||||
|
||||
WareHouse 和 Site 本质上是同一概念:都是定义一组固定放置位(slot),根据物理尺寸自行批量计算偏移坐标。WareHouse 只是多嵌套了一层 Deck 而已。推荐开发者直接根据实物测量数据计算各 slot 偏移量。
|
||||
|
||||
#### WareHouse(使用 warehouse_factory)
|
||||
|
||||
```python
|
||||
from unilabos.resources.warehouse import warehouse_factory
|
||||
from unilabos.registry.decorators import resource
|
||||
|
||||
|
||||
@resource(id="my_warehouse_4x4", category=["warehouse"], description="4x4 堆栈仓库")
|
||||
def my_warehouse_4x4(name: str) -> "WareHouse":
|
||||
return warehouse_factory(
|
||||
name=name,
|
||||
num_items_x=4, num_items_y=4, num_items_z=1,
|
||||
dx=10.0, dy=10.0, dz=10.0, # 第一个 slot 的起始偏移
|
||||
item_dx=147.0, item_dy=106.0, item_dz=130.0, # slot 间距
|
||||
resource_size_x=127.0, resource_size_y=85.0, resource_size_z=100.0, # slot 尺寸
|
||||
model="my_warehouse_4x4",
|
||||
col_offset=0, # 列标签起始偏移(0 → A01, 4 → A05)
|
||||
layout="row-major", # "row-major" 行优先 / "col-major" 列优先 / "vertical-col-major" 竖向
|
||||
)
|
||||
```
|
||||
|
||||
`warehouse_factory` 参数说明:
|
||||
- `dx/dy/dz`:第一个 slot 相对 WareHouse 原点的偏移(mm)
|
||||
- `item_dx/item_dy/item_dz`:相邻 slot 间距(mm),需根据实际物理间距测量
|
||||
- `resource_size_x/y/z`:每个 slot 的可放置区域尺寸
|
||||
- `layout`:影响 slot 标签和坐标映射
|
||||
- `"row-major"`:A01,A02,...,B01,B02,...(行优先,适合横向排列)
|
||||
- `"col-major"`:A01,B01,...,A02,B02,...(列优先)
|
||||
- `"vertical-col-major"`:竖向排列,y 坐标反向
|
||||
|
||||
#### Deck 组装 WareHouse
|
||||
|
||||
Deck 通过 `setup()` 将多个 WareHouse 放置到指定坐标:
|
||||
|
||||
```python
|
||||
from pylabrobot.resources import Deck, Coordinate
|
||||
from unilabos.registry.decorators import resource
|
||||
|
||||
|
||||
@resource(id="MyStation_Deck", category=["deck"], description="我的工作站 Deck")
|
||||
class MyStation_Deck(Deck):
|
||||
def __init__(self, name="MyStation_Deck", size_x=2700.0, size_y=1080.0, size_z=1500.0,
|
||||
category="deck", setup=False, **kwargs) -> None:
|
||||
super().__init__(name=name, size_x=size_x, size_y=size_y, size_z=size_z)
|
||||
if setup:
|
||||
self.setup()
|
||||
|
||||
def setup(self) -> None:
|
||||
self.warehouses = {
|
||||
"堆栈1左": my_warehouse_4x4("堆栈1左"),
|
||||
"堆栈1右": my_warehouse_4x4("堆栈1右"),
|
||||
}
|
||||
self.warehouse_locations = {
|
||||
"堆栈1左": Coordinate(-200.0, 400.0, 0.0), # 自行测量计算
|
||||
"堆栈1右": Coordinate(2350.0, 400.0, 0.0),
|
||||
}
|
||||
for wh_name, wh in self.warehouses.items():
|
||||
self.assign_child_resource(wh, location=self.warehouse_locations[wh_name])
|
||||
```
|
||||
|
||||
#### Site 模式(前端定向放置)
|
||||
|
||||
适用于有固定孔位/槽位的设备(如移液站 PRCXI 9300),Deck 通过 `sites` 列表定义前端展示的放置位,前端据此渲染可拖拽的孔位布局:
|
||||
|
||||
```python
|
||||
import collections
|
||||
from typing import Any, Dict, List, Optional
|
||||
from pylabrobot.resources import Deck, Resource, Coordinate
|
||||
from unilabos.registry.decorators import resource
|
||||
|
||||
|
||||
@resource(id="MyLabDeck", category=["deck"], description="带 Site 定向放置的 Deck")
|
||||
class MyLabDeck(Deck):
|
||||
# 根据设备台面实测批量计算各 slot 坐标偏移
|
||||
_DEFAULT_SITE_POSITIONS = [
|
||||
(0, 0, 0), (138, 0, 0), (276, 0, 0), (414, 0, 0), # T1-T4
|
||||
(0, 96, 0), (138, 96, 0), (276, 96, 0), (414, 96, 0), # T5-T8
|
||||
]
|
||||
_DEFAULT_SITE_SIZE = {"width": 128.0, "height": 86.0, "depth": 0}
|
||||
_DEFAULT_CONTENT_TYPE = ["plate", "tip_rack", "tube_rack", "adaptor"]
|
||||
|
||||
def __init__(self, name: str, size_x: float, size_y: float, size_z: float,
|
||||
sites: Optional[List[Dict[str, Any]]] = None, **kwargs):
|
||||
super().__init__(size_x, size_y, size_z, name)
|
||||
if sites is not None:
|
||||
self.sites = [dict(s) for s in sites]
|
||||
else:
|
||||
self.sites = []
|
||||
for i, (x, y, z) in enumerate(self._DEFAULT_SITE_POSITIONS):
|
||||
self.sites.append({
|
||||
"label": f"T{i + 1}", # 前端显示的槽位标签
|
||||
"visible": True, # 是否在前端可见
|
||||
"position": {"x": x, "y": y, "z": z}, # 槽位物理坐标
|
||||
"size": dict(self._DEFAULT_SITE_SIZE), # 槽位尺寸
|
||||
"content_type": list(self._DEFAULT_CONTENT_TYPE), # 允许放入的物料类型
|
||||
})
|
||||
self._ordering = collections.OrderedDict(
|
||||
(site["label"], None) for site in self.sites
|
||||
)
|
||||
|
||||
def assign_child_resource(self, resource: Resource,
|
||||
location: Optional[Coordinate] = None,
|
||||
reassign: bool = True,
|
||||
spot: Optional[int] = None):
|
||||
idx = spot
|
||||
if spot is None:
|
||||
for i, site in enumerate(self.sites):
|
||||
if site.get("label") == resource.name:
|
||||
idx = i
|
||||
break
|
||||
if idx is None:
|
||||
for i in range(len(self.sites)):
|
||||
if self._get_site_resource(i) is None:
|
||||
idx = i
|
||||
break
|
||||
if idx is None:
|
||||
raise ValueError(f"No available site for '{resource.name}'")
|
||||
loc = Coordinate(**self.sites[idx]["position"])
|
||||
super().assign_child_resource(resource, location=loc, reassign=reassign)
|
||||
|
||||
def serialize(self) -> dict:
|
||||
data = super().serialize()
|
||||
sites_out = []
|
||||
for i, site in enumerate(self.sites):
|
||||
occupied = self._get_site_resource(i)
|
||||
sites_out.append({
|
||||
"label": site["label"],
|
||||
"visible": site.get("visible", True),
|
||||
"occupied_by": occupied.name if occupied else None,
|
||||
"position": site["position"],
|
||||
"size": site["size"],
|
||||
"content_type": site["content_type"],
|
||||
})
|
||||
data["sites"] = sites_out
|
||||
return data
|
||||
```
|
||||
|
||||
**Site 字段说明:**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `label` | str | 槽位标签(如 `"T1"`),前端显示名称,也用于匹配 resource.name |
|
||||
| `visible` | bool | 是否在前端可见 |
|
||||
| `position` | dict | 物理坐标 `{x, y, z}`(mm),需自行测量计算偏移 |
|
||||
| `size` | dict | 槽位尺寸 `{width, height, depth}`(mm) |
|
||||
| `content_type` | list | 允许放入的物料类型,如 `["plate", "tip_rack", "tube_rack", "adaptor"]` |
|
||||
|
||||
**参考实现:** `unilabos/devices/liquid_handling/prcxi/prcxi.py` 中的 `PRCXI9300Deck`(4x4 共 16 个 site)。
|
||||
|
||||
---
|
||||
|
||||
## 文件位置
|
||||
|
||||
```
|
||||
unilabos/resources/
|
||||
├── <project>/ # 按项目分组
|
||||
│ ├── bottles.py # Bottle 工厂函数
|
||||
│ ├── bottle_carriers.py # Carrier 工厂函数
|
||||
│ ├── warehouses.py # WareHouse 工厂函数
|
||||
│ └── decks.py # Deck 类定义
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
# 资源可导入
|
||||
python -c "from unilabos.resources.my_project.bottles import My_Reagent_Bottle; print(My_Reagent_Bottle('test'))"
|
||||
|
||||
# 启动测试(AST 自动扫描)
|
||||
unilab -g <graph>.json
|
||||
```
|
||||
|
||||
仅在以下情况仍需 YAML:第三方库资源(如 pylabrobot 内置资源,无 `@resource` 装饰器)。
|
||||
|
||||
---
|
||||
|
||||
## 关键路径
|
||||
|
||||
| 内容 | 路径 |
|
||||
|------|------|
|
||||
| Bottle/Carrier 基类 | `unilabos/resources/itemized_carrier.py` |
|
||||
| WareHouse 基类 + 工厂 | `unilabos/resources/warehouse.py` |
|
||||
| PLR 注册 | `unilabos/resources/plr_additional_res_reg.py` |
|
||||
| 装饰器定义 | `unilabos/registry/decorators.py` |
|
||||
292
.cursor/skills/add-resource/reference.md
Normal file
292
.cursor/skills/add-resource/reference.md
Normal file
@@ -0,0 +1,292 @@
|
||||
# 资源高级参考
|
||||
|
||||
本文件是 SKILL.md 的补充,包含类继承体系、序列化/反序列化、Bioyond 物料同步、非瓶类资源和仓库工厂模式。Agent 在需要实现这些功能时按需阅读。
|
||||
|
||||
---
|
||||
|
||||
## 1. 类继承体系
|
||||
|
||||
```
|
||||
PyLabRobot
|
||||
├── Resource (PLR 基类)
|
||||
│ ├── Well
|
||||
│ │ └── Bottle (unilabos) → 瓶/小瓶/烧杯/反应器
|
||||
│ ├── Deck
|
||||
│ │ └── 自定义 Deck 类 (unilabos) → 工作站台面
|
||||
│ ├── ResourceHolder → 槽位占位符
|
||||
│ └── Container
|
||||
│ └── Battery (unilabos) → 组装好的电池
|
||||
│
|
||||
├── ItemizedCarrier (unilabos, 继承 Resource)
|
||||
│ ├── BottleCarrier (unilabos) → 瓶载架
|
||||
│ └── WareHouse (unilabos) → 堆栈仓库
|
||||
│
|
||||
├── ItemizedResource (PLR)
|
||||
│ └── MagazineHolder (unilabos) → 子弹夹载架
|
||||
│
|
||||
└── ResourceStack (PLR)
|
||||
└── Magazine (unilabos) → 子弹夹洞位
|
||||
```
|
||||
|
||||
### Bottle 类细节
|
||||
|
||||
```python
|
||||
class Bottle(Well):
|
||||
def __init__(self, name, diameter, height, max_volume,
|
||||
size_x=0.0, size_y=0.0, size_z=0.0,
|
||||
barcode=None, category="container", model=None, **kwargs):
|
||||
super().__init__(
|
||||
name=name,
|
||||
size_x=diameter, # PLR 用 diameter 作为 size_x/size_y
|
||||
size_y=diameter,
|
||||
size_z=height, # PLR 用 height 作为 size_z
|
||||
max_volume=max_volume,
|
||||
category=category,
|
||||
model=model,
|
||||
bottom_type="flat",
|
||||
cross_section_type="circle"
|
||||
)
|
||||
```
|
||||
|
||||
注意 `size_x = size_y = diameter`,`size_z = height`。
|
||||
|
||||
### ItemizedCarrier 核心方法
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `__getitem__(identifier)` | 通过索引或 Excel 标识(如 `"A01"`)访问槽位 |
|
||||
| `__setitem__(identifier, resource)` | 向槽位放入资源 |
|
||||
| `get_child_identifier(child)` | 获取子资源的标识符 |
|
||||
| `capacity` | 总槽位数 |
|
||||
| `sites` | 所有槽位字典 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 序列化与反序列化
|
||||
|
||||
### PLR ↔ UniLab 转换
|
||||
|
||||
| 函数 | 位置 | 方向 |
|
||||
|------|------|------|
|
||||
| `ResourceTreeSet.from_plr_resources(resources)` | `resource_tracker.py` | PLR → UniLab |
|
||||
| `ResourceTreeSet.to_plr_resources()` | `resource_tracker.py` | UniLab → PLR |
|
||||
|
||||
### `from_plr_resources` 流程
|
||||
|
||||
```
|
||||
PLR Resource
|
||||
↓ build_uuid_mapping (递归生成 UUID)
|
||||
↓ resource.serialize() → dict
|
||||
↓ resource.serialize_all_state() → states
|
||||
↓ resource_plr_inner (递归构建 ResourceDictInstance)
|
||||
ResourceTreeSet
|
||||
```
|
||||
|
||||
关键:每个 PLR 资源通过 `unilabos_uuid` 属性携带 UUID,`unilabos_extra` 携带扩展数据(如 `class` 名)。
|
||||
|
||||
### `to_plr_resources` 流程
|
||||
|
||||
```
|
||||
ResourceTreeSet
|
||||
↓ collect_node_data (收集 UUID、状态、扩展数据)
|
||||
↓ node_to_plr_dict (转为 PLR 字典格式)
|
||||
↓ find_subclass(type_name, PLRResource) (查找 PLR 子类)
|
||||
↓ sub_cls.deserialize(plr_dict) (反序列化)
|
||||
↓ loop_set_uuid, loop_set_extra (递归设置 UUID 和扩展)
|
||||
PLR Resource
|
||||
```
|
||||
|
||||
### Bottle 序列化
|
||||
|
||||
```python
|
||||
class Bottle(Well):
|
||||
def serialize(self) -> dict:
|
||||
data = super().serialize()
|
||||
return {**data, "diameter": self.diameter, "height": self.height}
|
||||
|
||||
@classmethod
|
||||
def deserialize(cls, data: dict, allow_marshal=False):
|
||||
barcode_data = data.pop("barcode", None)
|
||||
instance = super().deserialize(data, allow_marshal=allow_marshal)
|
||||
if barcode_data and isinstance(barcode_data, str):
|
||||
instance.barcode = barcode_data
|
||||
return instance
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Bioyond 物料同步
|
||||
|
||||
### 双向转换函数
|
||||
|
||||
| 函数 | 位置 | 方向 |
|
||||
|------|------|------|
|
||||
| `resource_bioyond_to_plr(materials, type_mapping, deck)` | `graphio.py` | Bioyond → PLR |
|
||||
| `resource_plr_to_bioyond(resources, type_mapping, warehouse_mapping)` | `graphio.py` | PLR → Bioyond |
|
||||
|
||||
### `resource_bioyond_to_plr` 流程
|
||||
|
||||
```
|
||||
Bioyond 物料列表
|
||||
↓ reverse_type_mapping: {typeName → (model, UUID)}
|
||||
↓ 对每个物料:
|
||||
typeName → 查映射 → model (如 "BIOYOND_PolymerStation_Reactor")
|
||||
initialize_resource({"name": unique_name, "class": model})
|
||||
↓ 设置 unilabos_extra (material_bioyond_id, material_bioyond_name 等)
|
||||
↓ 处理 detail (子物料/坐标)
|
||||
↓ 按 locationName 放入 deck.warehouses 对应槽位
|
||||
PLR 资源列表
|
||||
```
|
||||
|
||||
### `resource_plr_to_bioyond` 流程
|
||||
|
||||
```
|
||||
PLR 资源列表
|
||||
↓ 遍历每个资源:
|
||||
载架(capacity > 1): 生成 details 子物料 + 坐标
|
||||
单瓶: 直接映射
|
||||
↓ type_mapping 查找 typeId
|
||||
↓ warehouse_mapping 查找位置 UUID
|
||||
↓ 组装 Bioyond 格式 (name, typeName, typeId, quantity, Parameters, locations)
|
||||
Bioyond 物料列表
|
||||
```
|
||||
|
||||
### BioyondResourceSynchronizer
|
||||
|
||||
工作站通过 `ResourceSynchronizer` 自动同步物料:
|
||||
|
||||
```python
|
||||
class BioyondResourceSynchronizer(ResourceSynchronizer):
|
||||
def sync_from_external(self) -> bool:
|
||||
all_data = []
|
||||
all_data.extend(api_client.stock_material('{"typeMode": 0}')) # 耗材
|
||||
all_data.extend(api_client.stock_material('{"typeMode": 1}')) # 样品
|
||||
all_data.extend(api_client.stock_material('{"typeMode": 2}')) # 试剂
|
||||
unilab_resources = resource_bioyond_to_plr(
|
||||
all_data,
|
||||
type_mapping=self.workstation.bioyond_config["material_type_mappings"],
|
||||
deck=self.workstation.deck
|
||||
)
|
||||
# 更新 deck 上的资源
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 非瓶类资源
|
||||
|
||||
### ElectrodeSheet(极片)
|
||||
|
||||
路径:`unilabos/resources/battery/electrode_sheet.py`
|
||||
|
||||
```python
|
||||
class ElectrodeSheet(ResourcePLR):
|
||||
"""片状材料(极片、隔膜、弹片、垫片等)"""
|
||||
_unilabos_state = {
|
||||
"diameter": 0.0,
|
||||
"thickness": 0.0,
|
||||
"mass": 0.0,
|
||||
"material_type": "",
|
||||
"color": "",
|
||||
"info": "",
|
||||
}
|
||||
```
|
||||
|
||||
工厂函数:`PositiveCan`, `PositiveElectrode`, `NegativeCan`, `NegativeElectrode`, `SpringWasher`, `FlatWasher`, `AluminumFoil`
|
||||
|
||||
### Battery(电池)
|
||||
|
||||
```python
|
||||
class Battery(Container):
|
||||
"""组装好的电池"""
|
||||
_unilabos_state = {
|
||||
"color": "",
|
||||
"electrolyte_name": "",
|
||||
"open_circuit_voltage": 0.0,
|
||||
}
|
||||
```
|
||||
|
||||
### Magazine / MagazineHolder(子弹夹)
|
||||
|
||||
```python
|
||||
class Magazine(ResourceStack):
|
||||
"""子弹夹洞位,可堆叠 ElectrodeSheet"""
|
||||
# direction, max_sheets
|
||||
|
||||
class MagazineHolder(ItemizedResource):
|
||||
"""多洞位子弹夹"""
|
||||
# hole_diameter, hole_depth, max_sheets_per_hole
|
||||
```
|
||||
|
||||
工厂函数 `magazine_factory()` 用 `create_homogeneous_resources` 生成洞位,可选预填 `ElectrodeSheet` 或 `Battery`。
|
||||
|
||||
---
|
||||
|
||||
## 5. 仓库工厂模式参考
|
||||
|
||||
### 实际 warehouse 工厂函数示例
|
||||
|
||||
```python
|
||||
# 行优先 4x4 仓库
|
||||
def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
|
||||
return warehouse_factory(
|
||||
name=name,
|
||||
num_items_x=4, num_items_y=4, num_items_z=1,
|
||||
dx=10.0, dy=10.0, dz=10.0,
|
||||
item_dx=147.0, item_dy=106.0, item_dz=130.0,
|
||||
layout="row-major", # A01,A02,A03,A04, B01,...
|
||||
)
|
||||
|
||||
# 右侧 4x4 仓库(列名偏移)
|
||||
def bioyond_warehouse_1x4x4_right(name: str) -> WareHouse:
|
||||
return warehouse_factory(
|
||||
name=name,
|
||||
num_items_x=4, num_items_y=4, num_items_z=1,
|
||||
dx=10.0, dy=10.0, dz=10.0,
|
||||
item_dx=147.0, item_dy=106.0, item_dz=130.0,
|
||||
col_offset=4, # A05,A06,A07,A08
|
||||
layout="row-major",
|
||||
)
|
||||
|
||||
# 竖向仓库(站内试剂存放)
|
||||
def bioyond_warehouse_reagent_storage(name: str) -> WareHouse:
|
||||
return warehouse_factory(
|
||||
name=name,
|
||||
num_items_x=1, num_items_y=2, num_items_z=1,
|
||||
dx=10.0, dy=10.0, dz=10.0,
|
||||
item_dx=147.0, item_dy=106.0, item_dz=130.0,
|
||||
layout="vertical-col-major",
|
||||
)
|
||||
|
||||
# 行偏移(F 行开始)
|
||||
def bioyond_warehouse_5x3x1(name: str, row_offset: int = 0) -> WareHouse:
|
||||
return warehouse_factory(
|
||||
name=name,
|
||||
num_items_x=3, num_items_y=5, num_items_z=1,
|
||||
dx=10.0, dy=10.0, dz=10.0,
|
||||
item_dx=159.0, item_dy=183.0, item_dz=130.0,
|
||||
row_offset=row_offset, # 0→A行起,5→F行起
|
||||
layout="row-major",
|
||||
)
|
||||
```
|
||||
|
||||
### layout 类型说明
|
||||
|
||||
| layout | 命名顺序 | 适用场景 |
|
||||
|--------|---------|---------|
|
||||
| `col-major` (默认) | A01,B01,C01,D01, A02,B02,... | 列优先,标准堆栈 |
|
||||
| `row-major` | A01,A02,A03,A04, B01,B02,... | 行优先,Bioyond 前端展示 |
|
||||
| `vertical-col-major` | 竖向排列,标签从底部开始 | 竖向仓库(试剂存放、测密度) |
|
||||
|
||||
---
|
||||
|
||||
## 6. 关键路径
|
||||
|
||||
| 内容 | 路径 |
|
||||
|------|------|
|
||||
| Bottle/Carrier 基类 | `unilabos/resources/itemized_carrier.py` |
|
||||
| WareHouse 类 + 工厂 | `unilabos/resources/warehouse.py` |
|
||||
| ResourceTreeSet 转换 | `unilabos/resources/resource_tracker.py` |
|
||||
| Bioyond 物料转换 | `unilabos/resources/graphio.py` |
|
||||
| Bioyond 仓库定义 | `unilabos/resources/bioyond/warehouses.py` |
|
||||
| 电池资源 | `unilabos/resources/battery/` |
|
||||
| PLR 注册 | `unilabos/resources/plr_additional_res_reg.py` |
|
||||
626
.cursor/skills/add-workstation/SKILL.md
Normal file
626
.cursor/skills/add-workstation/SKILL.md
Normal file
@@ -0,0 +1,626 @@
|
||||
---
|
||||
name: add-workstation
|
||||
description: Guide for adding new workstations to Uni-Lab-OS (接入新工作站). Uses @device decorator + AST auto-scanning. Walks through workstation type, sub-device composition, driver creation, deck setup, and graph file. Use when the user wants to add a workstation, create a workstation driver, configure a station with sub-devices, or mentions 工作站/工站/station/workstation.
|
||||
---
|
||||
|
||||
# Uni-Lab-OS 工作站接入指南
|
||||
|
||||
工作站(workstation)是组合多个子设备的大型设备,拥有独立的物料管理系统和工作流引擎。使用 `@device` 装饰器注册,AST 自动扫描生成注册表。
|
||||
|
||||
---
|
||||
|
||||
## 工作站类型
|
||||
|
||||
| 类型 | 基类 | 适用场景 |
|
||||
| ------------------- | ----------------- | ---------------------------------- |
|
||||
| **Protocol 工作站** | `ProtocolNode` | 标准化学操作协议(泵转移、过滤等) |
|
||||
| **外部系统工作站** | `WorkstationBase` | 与外部 LIMS/MES 对接 |
|
||||
| **硬件控制工作站** | `WorkstationBase` | 直接控制 PLC/硬件 |
|
||||
|
||||
---
|
||||
|
||||
## @device 装饰器(工作站)
|
||||
|
||||
工作站也使用 `@device` 装饰器注册,参数与普通设备一致:
|
||||
|
||||
```python
|
||||
@device(
|
||||
id="my_workstation", # 注册表唯一标识(必填)
|
||||
category=["workstation"], # 分类标签
|
||||
description="我的工作站",
|
||||
)
|
||||
```
|
||||
|
||||
如果一个工作站类支持多个具体变体,可使用 `ids` / `id_meta`,与设备的用法相同(参见 add-device SKILL)。
|
||||
|
||||
---
|
||||
|
||||
## 工作站驱动模板
|
||||
|
||||
### 模板 A:基于外部系统的工作站
|
||||
|
||||
```python
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
from pylabrobot.resources import Deck
|
||||
|
||||
from unilabos.registry.decorators import device, topic_config, not_action
|
||||
from unilabos.devices.workstation.workstation_base import WorkstationBase
|
||||
|
||||
try:
|
||||
from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode
|
||||
except ImportError:
|
||||
ROS2WorkstationNode = None
|
||||
|
||||
|
||||
@device(id="my_workstation", category=["workstation"], description="我的工作站")
|
||||
class MyWorkstation(WorkstationBase):
|
||||
_ros_node: "ROS2WorkstationNode"
|
||||
|
||||
def __init__(self, config=None, deck=None, protocol_type=None, **kwargs):
|
||||
super().__init__(deck=deck, **kwargs)
|
||||
self.config = config or {}
|
||||
self.logger = logging.getLogger("MyWorkstation")
|
||||
self.api_host = self.config.get("api_host", "")
|
||||
self._status = "Idle"
|
||||
|
||||
@not_action
|
||||
def post_init(self, ros_node: "ROS2WorkstationNode"):
|
||||
super().post_init(ros_node)
|
||||
self._ros_node = ros_node
|
||||
|
||||
async def scheduler_start(self, **kwargs) -> Dict[str, Any]:
|
||||
"""注册为工作站动作"""
|
||||
return {"success": True}
|
||||
|
||||
async def create_order(self, json_str: str, **kwargs) -> Dict[str, Any]:
|
||||
"""注册为工作站动作"""
|
||||
return {"success": True}
|
||||
|
||||
@property
|
||||
@topic_config()
|
||||
def workflow_sequence(self) -> str:
|
||||
return "[]"
|
||||
|
||||
@property
|
||||
@topic_config()
|
||||
def material_info(self) -> str:
|
||||
return "{}"
|
||||
```
|
||||
|
||||
### 模板 B:Protocol 工作站
|
||||
|
||||
直接使用 `ProtocolNode`,通常不需要自定义驱动类:
|
||||
|
||||
```python
|
||||
from unilabos.devices.workstation.workstation_base import ProtocolNode
|
||||
```
|
||||
|
||||
在图文件中配置 `protocol_type` 即可。
|
||||
|
||||
---
|
||||
|
||||
## 子设备访问(sub_devices)
|
||||
|
||||
工站初始化子设备后,所有子设备实例存储在 `self._ros_node.sub_devices` 字典中(key 为设备 id,value 为 `ROS2DeviceNode` 实例)。工站的驱动类可以直接获取子设备实例来调用其方法:
|
||||
|
||||
```python
|
||||
# 在工站驱动类的方法中访问子设备
|
||||
sub = self._ros_node.sub_devices["pump_1"]
|
||||
|
||||
# .driver_instance — 子设备的驱动实例(即设备 Python 类的实例)
|
||||
sub.driver_instance.some_method(arg1, arg2)
|
||||
|
||||
# .ros_node_instance — 子设备的 ROS2 节点实例
|
||||
sub.ros_node_instance._action_value_mappings # 查看子设备支持的 action
|
||||
```
|
||||
|
||||
**常见用法**:
|
||||
|
||||
```python
|
||||
class MyWorkstation(WorkstationBase):
|
||||
def my_protocol(self, **kwargs):
|
||||
# 获取子设备驱动实例
|
||||
pump = self._ros_node.sub_devices["pump_1"].driver_instance
|
||||
heater = self._ros_node.sub_devices["heater_1"].driver_instance
|
||||
|
||||
# 直接调用子设备方法
|
||||
pump.aspirate(volume=100)
|
||||
heater.set_temperature(80)
|
||||
```
|
||||
|
||||
> 参考实现:`unilabos/devices/workstation/bioyond_studio/reaction_station/reaction_station.py` 中通过 `self._ros_node.sub_devices.get(reactor_id)` 获取子反应器实例并更新数据。
|
||||
|
||||
---
|
||||
|
||||
## 硬件通信接口(hardware_interface)
|
||||
|
||||
硬件控制型工作站通常需要通过串口(Serial)、Modbus 等通信协议控制多个子设备。Uni-Lab-OS 通过 **通信设备代理** 机制实现端口共享:一个串口只创建一个 `serial` 节点,多个子设备共享这个通信实例。
|
||||
|
||||
### 工作原理
|
||||
|
||||
`ROS2WorkstationNode` 初始化时分两轮遍历子设备(`workstation.py`):
|
||||
|
||||
**第一轮 — 初始化所有子设备**:按 `children` 顺序调用 `initialize_device()`,通信设备(`serial_` / `io_` 开头的 id)优先完成初始化,创建 `serial.Serial()` 实例。其他子设备此时 `self.hardware_interface = "serial_pump"`(字符串)。
|
||||
|
||||
**第二轮 — 代理替换**:遍历所有已初始化的子设备,读取子设备的 `_hardware_interface` 配置:
|
||||
|
||||
```
|
||||
hardware_interface = d.ros_node_instance._hardware_interface
|
||||
# → {"name": "hardware_interface", "read": "send_command", "write": "send_command"}
|
||||
```
|
||||
|
||||
1. 取 `name` 字段对应的属性值:`name_value = getattr(driver, hardware_interface["name"])`
|
||||
- 如果 `name_value` 是字符串且该字符串是某个子设备的 id → 触发代理替换
|
||||
2. 从通信设备获取真正的 `read`/`write` 方法
|
||||
3. 用 `setattr(driver, read_method, _read)` 将通信设备的方法绑定到子设备上
|
||||
|
||||
因此:
|
||||
|
||||
- **通信设备 id 必须与子设备 config 中填的字符串完全一致**(如 `"serial_pump"`)
|
||||
- **通信设备 id 必须以 `serial_` 或 `io_` 开头**(否则第一轮不会被识别为通信设备)
|
||||
- **通信设备必须在 `children` 列表中排在最前面**,确保先初始化
|
||||
|
||||
### HardwareInterface 参数说明
|
||||
|
||||
```python
|
||||
from unilabos.registry.decorators import HardwareInterface
|
||||
|
||||
HardwareInterface(
|
||||
name="hardware_interface", # __init__ 中接收通信实例的属性名
|
||||
read="send_command", # 通信设备上暴露的读方法名
|
||||
write="send_command", # 通信设备上暴露的写方法名
|
||||
extra_info=["list_ports"], # 可选:额外暴露的方法
|
||||
)
|
||||
```
|
||||
|
||||
**`name` 字段的含义**:对应设备类 `__init__` 中,用于保存通信实例的**属性名**。系统据此知道要替换哪个属性。大部分设备直接用 `"hardware_interface"`,也可以自定义(如 `"io_device_port"`)。
|
||||
|
||||
### 示例 1:泵(name="hardware_interface")
|
||||
|
||||
```python
|
||||
from unilabos.registry.decorators import device, HardwareInterface
|
||||
|
||||
@device(
|
||||
id="my_pump",
|
||||
category=["pump_and_valve"],
|
||||
hardware_interface=HardwareInterface(
|
||||
name="hardware_interface",
|
||||
read="send_command",
|
||||
write="send_command",
|
||||
),
|
||||
)
|
||||
class MyPump:
|
||||
def __init__(self, port=None, address="1", **kwargs):
|
||||
# name="hardware_interface" → 系统替换 self.hardware_interface
|
||||
self.hardware_interface = port # 初始为字符串 "serial_pump",启动后被替换为 Serial 实例
|
||||
self.address = address
|
||||
|
||||
def send_command(self, command: str):
|
||||
full_command = f"/{self.address}{command}\r\n"
|
||||
self.hardware_interface.write(bytearray(full_command, "ascii"))
|
||||
return self.hardware_interface.read_until(b"\n")
|
||||
```
|
||||
|
||||
### 示例 2:电磁阀(name="io_device_port",自定义属性名)
|
||||
|
||||
```python
|
||||
@device(
|
||||
id="solenoid_valve",
|
||||
category=["pump_and_valve"],
|
||||
hardware_interface=HardwareInterface(
|
||||
name="io_device_port", # 自定义属性名 → 系统替换 self.io_device_port
|
||||
read="read_io_coil",
|
||||
write="write_io_coil",
|
||||
),
|
||||
)
|
||||
class SolenoidValve:
|
||||
def __init__(self, io_device_port: str = None, **kwargs):
|
||||
# name="io_device_port" → 图文件 config 中用 "io_device_port": "io_board_1"
|
||||
self.io_device_port = io_device_port # 初始为字符串,系统替换为 Modbus 实例
|
||||
```
|
||||
|
||||
### Serial 通信设备(class="serial")
|
||||
|
||||
`serial` 是 Uni-Lab-OS 内置的通信代理设备,代码位于 `unilabos/ros/nodes/presets/serial_node.py`:
|
||||
|
||||
```python
|
||||
from serial import Serial, SerialException
|
||||
from threading import Lock
|
||||
|
||||
class ROS2SerialNode(BaseROS2DeviceNode):
|
||||
def __init__(self, device_id, registry_name, port: str, baudrate: int = 9600, **kwargs):
|
||||
self.port = port
|
||||
self.baudrate = baudrate
|
||||
self._hardware_interface = {
|
||||
"name": "hardware_interface",
|
||||
"write": "send_command",
|
||||
"read": "read_data",
|
||||
}
|
||||
self._query_lock = Lock()
|
||||
|
||||
self.hardware_interface = Serial(baudrate=baudrate, port=port)
|
||||
|
||||
BaseROS2DeviceNode.__init__(
|
||||
self, driver_instance=self, registry_name=registry_name,
|
||||
device_id=device_id, status_types={}, action_value_mappings={},
|
||||
hardware_interface=self._hardware_interface, print_publish=False,
|
||||
)
|
||||
self.create_service(SerialCommand, "serialwrite", self.handle_serial_request)
|
||||
|
||||
def send_command(self, command: str):
|
||||
with self._query_lock:
|
||||
self.hardware_interface.write(bytearray(f"{command}\n", "ascii"))
|
||||
return self.hardware_interface.read_until(b"\n").decode()
|
||||
|
||||
def read_data(self):
|
||||
with self._query_lock:
|
||||
return self.hardware_interface.read_until(b"\n").decode()
|
||||
```
|
||||
|
||||
在图文件中使用 `"class": "serial"` 即可创建串口代理:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "serial_pump",
|
||||
"class": "serial",
|
||||
"parent": "my_station",
|
||||
"config": { "port": "COM7", "baudrate": 9600 }
|
||||
}
|
||||
```
|
||||
|
||||
### 图文件配置
|
||||
|
||||
**通信设备必须在 `children` 列表中排在最前面**,确保先于其他子设备初始化:
|
||||
|
||||
```json
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "my_station",
|
||||
"class": "workstation",
|
||||
"children": ["serial_pump", "pump_1", "pump_2"],
|
||||
"config": { "protocol_type": ["PumpTransferProtocol"] }
|
||||
},
|
||||
{
|
||||
"id": "serial_pump",
|
||||
"class": "serial",
|
||||
"parent": "my_station",
|
||||
"config": { "port": "COM7", "baudrate": 9600 }
|
||||
},
|
||||
{
|
||||
"id": "pump_1",
|
||||
"class": "syringe_pump_with_valve.runze.SY03B-T08",
|
||||
"parent": "my_station",
|
||||
"config": { "port": "serial_pump", "address": "1", "max_volume": 25.0 }
|
||||
},
|
||||
{
|
||||
"id": "pump_2",
|
||||
"class": "syringe_pump_with_valve.runze.SY03B-T08",
|
||||
"parent": "my_station",
|
||||
"config": { "port": "serial_pump", "address": "2", "max_volume": 25.0 }
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"source": "pump_1",
|
||||
"target": "serial_pump",
|
||||
"type": "communication",
|
||||
"port": { "pump_1": "port", "serial_pump": "port" }
|
||||
},
|
||||
{
|
||||
"source": "pump_2",
|
||||
"target": "serial_pump",
|
||||
"type": "communication",
|
||||
"port": { "pump_2": "port", "serial_pump": "port" }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 通信协议速查
|
||||
|
||||
| 协议 | config 参数 | 依赖包 | 通信设备 class |
|
||||
| -------------------- | ------------------------------ | ---------- | -------------------------- |
|
||||
| Serial (RS232/RS485) | `port`, `baudrate` | `pyserial` | `serial` |
|
||||
| Modbus RTU | `port`, `baudrate`, `slave_id` | `pymodbus` | `device_comms/modbus_plc/` |
|
||||
| Modbus TCP | `host`, `port`, `slave_id` | `pymodbus` | `device_comms/modbus_plc/` |
|
||||
| TCP Socket | `host`, `port` | stdlib | 自定义 |
|
||||
| HTTP API | `url`, `token` | `requests` | `device_comms/rpc.py` |
|
||||
|
||||
参考实现:`unilabos/test/experiments/Grignard_flow_batchreact_single_pumpvalve.json`
|
||||
|
||||
---
|
||||
|
||||
## Deck 与物料生命周期
|
||||
|
||||
### 1. Deck 入参与两种初始化模式
|
||||
|
||||
系统根据设备节点 `config.deck` 的写法,自动反序列化 Deck 实例后传入 `__init__` 的 `deck` 参数。目前 `deck` 是固定字段名,只支持一个主 Deck。建议一个设备拥有一个台面,台面上抽象二级、三级子物料。
|
||||
|
||||
有两种初始化模式:
|
||||
|
||||
#### init 初始化(推荐)
|
||||
|
||||
`config.deck` 直接包含 `_resource_type` + `_resource_child_name`,系统先用 Deck 节点的 `config` 调用 Deck 类的 `__init__` 反序列化,再将实例传入设备的 `deck` 参数。子物料随 Deck 的 `children` 一起反序列化。
|
||||
|
||||
```json
|
||||
"config": {
|
||||
"deck": {
|
||||
"_resource_type": "unilabos.devices.liquid_handling.prcxi.prcxi:PRCXI9300Deck",
|
||||
"_resource_child_name": "PRCXI_Deck"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### deserialize 初始化
|
||||
|
||||
`config.deck` 用 `data` 包裹一层,系统走 `deserialize` 路径,可传入更多参数(如 `allow_marshal` 等):
|
||||
|
||||
```json
|
||||
"config": {
|
||||
"deck": {
|
||||
"data": {
|
||||
"_resource_child_name": "YB_Bioyond_Deck",
|
||||
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_YB_Deck"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
没有特殊需求时推荐 init 初始化。
|
||||
|
||||
#### config.deck 字段说明
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `_resource_type` | Deck 类的完整模块路径(`module:ClassName`) |
|
||||
| `_resource_child_name` | 对应图文件中 Deck 节点的 `id`,建立父子关联 |
|
||||
|
||||
#### 设备 __init__ 接收
|
||||
|
||||
```python
|
||||
def __init__(self, config=None, deck=None, protocol_type=None, **kwargs):
|
||||
super().__init__(deck=deck, **kwargs)
|
||||
# deck 已经是反序列化后的 Deck 实例
|
||||
# → PRCXI9300Deck / BIOYOND_YB_Deck 等
|
||||
```
|
||||
|
||||
#### Deck 节点(图文件中)
|
||||
|
||||
Deck 节点作为设备的 `children` 之一,`parent` 指向设备 id:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "PRCXI_Deck",
|
||||
"parent": "PRCXI",
|
||||
"type": "deck",
|
||||
"class": "",
|
||||
"children": [],
|
||||
"config": {
|
||||
"type": "PRCXI9300Deck",
|
||||
"size_x": 542, "size_y": 374, "size_z": 0,
|
||||
"category": "deck",
|
||||
"sites": [...]
|
||||
},
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
- `config` 中的字段会传入 Deck 类的 `__init__`(因此 `__init__` 必须能接受所有 `serialize()` 输出的字段)
|
||||
- `children` 初始为空时,由同步器或手动初始化填充
|
||||
- `config.type` 填 Deck 类名
|
||||
|
||||
### 2. Deck 为空时自行初始化
|
||||
|
||||
如果 Deck 节点的 `children` 为空,工作站需在 `post_init` 或首次同步时自行初始化内容:
|
||||
|
||||
```python
|
||||
@not_action
|
||||
def post_init(self, ros_node):
|
||||
super().post_init(ros_node)
|
||||
if self.deck and not self.deck.children:
|
||||
self._initialize_default_deck()
|
||||
|
||||
def _initialize_default_deck(self):
|
||||
from my_labware import My_TipRack, My_Plate
|
||||
self.deck.assign_child_resource(My_TipRack("T1"), spot=0)
|
||||
self.deck.assign_child_resource(My_Plate("T2"), spot=1)
|
||||
```
|
||||
|
||||
### 3. 物料双向同步
|
||||
|
||||
当工作站对接外部系统(LIMS/MES)时,需要实现 `ResourceSynchronizer` 处理双向物料同步:
|
||||
|
||||
```python
|
||||
from unilabos.devices.workstation.workstation_base import ResourceSynchronizer
|
||||
|
||||
class MyResourceSynchronizer(ResourceSynchronizer):
|
||||
def sync_from_external(self) -> bool:
|
||||
"""从外部系统同步到 self.workstation.deck"""
|
||||
external_data = self._query_external_materials()
|
||||
# 以外部工站为准:根据外部数据反向创建 PLR 资源实例
|
||||
for item in external_data:
|
||||
cls = self._resolve_resource_class(item["type"])
|
||||
resource = cls(name=item["name"], **item["params"])
|
||||
self.workstation.deck.assign_child_resource(resource, spot=item["slot"])
|
||||
return True
|
||||
|
||||
def sync_to_external(self, resource) -> bool:
|
||||
"""将 UniLab 侧物料变更同步到外部系统"""
|
||||
# 以 UniLab 为准:将 PLR 资源转为外部格式并推送
|
||||
external_format = self._convert_to_external(resource)
|
||||
return self._push_to_external(external_format)
|
||||
|
||||
def handle_external_change(self, change_info) -> bool:
|
||||
"""处理外部系统主动推送的变更"""
|
||||
return True
|
||||
```
|
||||
|
||||
同步策略取决于业务场景:
|
||||
|
||||
- **以外部工站为准**:从外部 API 查询物料数据,反向创建对应的 PLR 资源实例放到 Deck 上
|
||||
- **以 UniLab 为准**:UniLab 侧的物料变更通过 `sync_to_external` 推送到外部系统
|
||||
|
||||
在工作站 `post_init` 中初始化同步器:
|
||||
|
||||
```python
|
||||
@not_action
|
||||
def post_init(self, ros_node):
|
||||
super().post_init(ros_node)
|
||||
self.resource_synchronizer = MyResourceSynchronizer(self)
|
||||
self.resource_synchronizer.sync_from_external()
|
||||
```
|
||||
|
||||
### 4. 序列化与持久化(serialize / serialize_state)
|
||||
|
||||
资源类需正确实现序列化,系统据此完成持久化和前端同步。
|
||||
|
||||
**`serialize()`** — 输出资源的结构信息(`config` 层),反序列化时作为 `__init__` 的入参回传。因此 **`__init__` 必须通过 `**kwargs`接受`serialize()` 输出的所有字段\*\*,即使当前不使用:
|
||||
|
||||
```python
|
||||
class MyDeck(Deck):
|
||||
def __init__(self, name, size_x, size_y, size_z,
|
||||
sites=None, # serialize() 输出的字段
|
||||
rotation=None, # serialize() 输出的字段
|
||||
barcode=None, # serialize() 输出的字段
|
||||
**kwargs): # 兜底:接受所有未知的 serialize 字段
|
||||
super().__init__(size_x, size_y, size_z, name)
|
||||
# ...
|
||||
|
||||
def serialize(self) -> dict:
|
||||
data = super().serialize()
|
||||
data["sites"] = [...] # 自定义字段
|
||||
return data
|
||||
```
|
||||
|
||||
**`serialize_state()`** — 输出资源的运行时状态(`data` 层),用于持久化可变信息。`data` 中的内容会被正确保存和恢复:
|
||||
|
||||
```python
|
||||
class MyPlate(Plate):
|
||||
def __init__(self, name, size_x, size_y, size_z,
|
||||
material_info=None, **kwargs):
|
||||
super().__init__(name, size_x, size_y, size_z, **kwargs)
|
||||
self._unilabos_state = {}
|
||||
if material_info:
|
||||
self._unilabos_state["Material"] = material_info
|
||||
|
||||
def serialize_state(self) -> Dict[str, Any]:
|
||||
data = super().serialize_state()
|
||||
data.update(self._unilabos_state)
|
||||
return data
|
||||
```
|
||||
|
||||
关键要点:
|
||||
|
||||
- `serialize()` 输出的所有字段都会作为 `config` 回传到 `__init__`,所以 `__init__` 必须能接受它们(显式声明或 `**kwargs`)
|
||||
- `serialize_state()` 输出的 `data` 用于持久化运行时状态(如物料信息、液体量等)
|
||||
- `_unilabos_state` 中只存可 JSON 序列化的基本类型(str, int, float, bool, list, dict, None)
|
||||
|
||||
### 5. 子物料自动同步
|
||||
|
||||
子物料(Bottle、Plate、TipRack 等)放到 Deck 上后,系统会自动将其同步到前端的 Deck 视图。只需保证资源类正确实现了 `serialize()` / `serialize_state()` 和反序列化即可。
|
||||
|
||||
### 6. 图文件配置(参考 prcxi_9320_slim.json)
|
||||
|
||||
```json
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "my_station",
|
||||
"type": "device",
|
||||
"class": "my_workstation",
|
||||
"config": {
|
||||
"deck": {
|
||||
"_resource_type": "unilabos.resources.my_module:MyDeck",
|
||||
"_resource_child_name": "my_deck"
|
||||
},
|
||||
"host": "10.20.30.1",
|
||||
"port": 9999
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "my_deck",
|
||||
"parent": "my_station",
|
||||
"type": "deck",
|
||||
"class": "",
|
||||
"children": [],
|
||||
"config": {
|
||||
"type": "MyLabDeck",
|
||||
"size_x": 542,
|
||||
"size_y": 374,
|
||||
"size_z": 0,
|
||||
"category": "deck",
|
||||
"sites": [
|
||||
{
|
||||
"label": "T1",
|
||||
"visible": true,
|
||||
"occupied_by": null,
|
||||
"position": { "x": 0, "y": 0, "z": 0 },
|
||||
"size": { "width": 128.0, "height": 86, "depth": 0 },
|
||||
"content_type": ["plate", "tip_rack", "tube_rack", "adaptor"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"data": {}
|
||||
}
|
||||
],
|
||||
"edges": []
|
||||
}
|
||||
```
|
||||
|
||||
Deck 节点要点:
|
||||
|
||||
- `config.type` 填 Deck 类名(如 `"PRCXI9300Deck"`)
|
||||
- `config.sites` 完整列出所有 site(从 Deck 类的 `serialize()` 输出获取)
|
||||
- `children` 初始为空(由同步器或手动初始化填充)
|
||||
- 设备节点 `config.deck._resource_type` 指向 Deck 类的完整模块路径
|
||||
|
||||
---
|
||||
|
||||
## 子设备
|
||||
|
||||
子设备按标准设备接入流程创建(参见 add-device SKILL),使用 `@device` 装饰器。
|
||||
|
||||
子设备约束:
|
||||
|
||||
- 图文件中 `parent` 指向工作站 ID
|
||||
- 在工作站 `children` 数组中列出
|
||||
|
||||
---
|
||||
|
||||
## 关键规则
|
||||
|
||||
1. **`__init__` 必须接受 `deck` 和 `**kwargs`** — `WorkstationBase.**init**`需要`deck` 参数
|
||||
2. **Deck 通过 `config.deck._resource_type` 反序列化传入** — 不要在 `__init__` 中手动创建 Deck
|
||||
3. **Deck 为空时自行初始化内容** — 在 `post_init` 中检查并填充默认物料
|
||||
4. **外部同步实现 `ResourceSynchronizer`** — `sync_from_external` / `sync_to_external`
|
||||
5. **通过 `self._children` 访问子设备** — 不要自行维护子设备引用
|
||||
6. **`post_init` 中启动后台服务** — 不要在 `__init__` 中启动网络连接
|
||||
7. **异步方法使用 `await self._ros_node.sleep()`** — 禁止 `time.sleep()` 和 `asyncio.sleep()`
|
||||
8. **使用 `@not_action` 标记非动作方法** — `post_init`, `initialize`, `cleanup`
|
||||
9. **子物料保证正确 serialize/deserialize** — 系统自动同步到前端 Deck 视图
|
||||
|
||||
---
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
# 模块可导入
|
||||
python -c "from unilabos.devices.workstation.<name>.<name> import <ClassName>"
|
||||
|
||||
# 启动测试(AST 自动扫描)
|
||||
unilab -g <graph>.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 现有工作站参考
|
||||
|
||||
| 工作站 | 驱动类 | 类型 |
|
||||
| -------------- | ----------------------------- | -------- |
|
||||
| Protocol 通用 | `ProtocolNode` | Protocol |
|
||||
| Bioyond 反应站 | `BioyondReactionStation` | 外部系统 |
|
||||
| 纽扣电池组装 | `CoinCellAssemblyWorkstation` | 硬件控制 |
|
||||
|
||||
参考路径:`unilabos/devices/workstation/` 目录下各工作站实现。
|
||||
371
.cursor/skills/add-workstation/reference.md
Normal file
371
.cursor/skills/add-workstation/reference.md
Normal file
@@ -0,0 +1,371 @@
|
||||
# 工作站高级模式参考
|
||||
|
||||
本文件是 SKILL.md 的补充,包含外部系统集成、物料同步、配置结构等高级模式。
|
||||
Agent 在需要实现这些功能时按需阅读。
|
||||
|
||||
---
|
||||
|
||||
## 1. 外部系统集成模式
|
||||
|
||||
### 1.1 RPC 客户端
|
||||
|
||||
与外部 LIMS/MES 系统通信的标准模式。继承 `BaseRequest`,所有接口统一用 POST。
|
||||
|
||||
```python
|
||||
from unilabos.device_comms.rpc import BaseRequest
|
||||
|
||||
|
||||
class MySystemRPC(BaseRequest):
|
||||
"""外部系统 RPC 客户端"""
|
||||
|
||||
def __init__(self, host: str, api_key: str):
|
||||
super().__init__(host)
|
||||
self.api_key = api_key
|
||||
|
||||
def _request(self, endpoint: str, data: dict = None) -> dict:
|
||||
return self.post(
|
||||
url=f"{self.host}/api/{endpoint}",
|
||||
params={
|
||||
"apiKey": self.api_key,
|
||||
"requestTime": self.get_current_time_iso8601(),
|
||||
"data": data or {},
|
||||
},
|
||||
)
|
||||
|
||||
def query_status(self) -> dict:
|
||||
return self._request("status/query")
|
||||
|
||||
def create_order(self, order_data: dict) -> dict:
|
||||
return self._request("order/create", order_data)
|
||||
```
|
||||
|
||||
参考:`unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py`(`BioyondV1RPC`)
|
||||
|
||||
### 1.2 HTTP 回调服务
|
||||
|
||||
接收外部系统报送的标准模式。使用 `WorkstationHTTPService`,在 `post_init` 中启动。
|
||||
|
||||
```python
|
||||
from unilabos.devices.workstation.workstation_http_service import WorkstationHTTPService
|
||||
|
||||
|
||||
class MyWorkstation(WorkstationBase):
|
||||
def __init__(self, config=None, deck=None, **kwargs):
|
||||
super().__init__(deck=deck, **kwargs)
|
||||
self.config = config or {}
|
||||
http_cfg = self.config.get("http_service_config", {})
|
||||
self._http_service_config = {
|
||||
"host": http_cfg.get("http_service_host", "127.0.0.1"),
|
||||
"port": http_cfg.get("http_service_port", 8080),
|
||||
}
|
||||
self.http_service = None
|
||||
|
||||
def post_init(self, ros_node):
|
||||
super().post_init(ros_node)
|
||||
self.http_service = WorkstationHTTPService(
|
||||
workstation_instance=self,
|
||||
host=self._http_service_config["host"],
|
||||
port=self._http_service_config["port"],
|
||||
)
|
||||
self.http_service.start()
|
||||
```
|
||||
|
||||
**HTTP 服务路由**(固定端点,由 `WorkstationHTTPHandler` 自动分发):
|
||||
|
||||
| 端点 | 调用的工作站方法 |
|
||||
|------|-----------------|
|
||||
| `/report/step_finish` | `process_step_finish_report(report_request)` |
|
||||
| `/report/sample_finish` | `process_sample_finish_report(report_request)` |
|
||||
| `/report/order_finish` | `process_order_finish_report(report_request, used_materials)` |
|
||||
| `/report/material_change` | `process_material_change_report(report_data)` |
|
||||
| `/report/error_handling` | `handle_external_error(error_data)` |
|
||||
|
||||
实现对应方法即可接收回调:
|
||||
|
||||
```python
|
||||
def process_step_finish_report(self, report_request) -> Dict[str, Any]:
|
||||
"""处理步骤完成报告"""
|
||||
step_name = report_request.data.get("stepName")
|
||||
return {"success": True, "message": f"步骤 {step_name} 已处理"}
|
||||
|
||||
def process_order_finish_report(self, report_request, used_materials) -> Dict[str, Any]:
|
||||
"""处理订单完成报告"""
|
||||
order_code = report_request.data.get("orderCode")
|
||||
return {"success": True}
|
||||
```
|
||||
|
||||
参考:`unilabos/devices/workstation/workstation_http_service.py`
|
||||
|
||||
### 1.3 连接监控
|
||||
|
||||
独立线程周期性检测外部系统连接状态,状态变化时发布 ROS 事件。
|
||||
|
||||
```python
|
||||
class ConnectionMonitor:
|
||||
def __init__(self, workstation, check_interval=30):
|
||||
self.workstation = workstation
|
||||
self.check_interval = check_interval
|
||||
self._running = False
|
||||
self._thread = None
|
||||
|
||||
def start(self):
|
||||
self._running = True
|
||||
self._thread = threading.Thread(target=self._monitor_loop, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def _monitor_loop(self):
|
||||
while self._running:
|
||||
try:
|
||||
# 调用外部系统接口检测连接
|
||||
self.workstation.hardware_interface.ping()
|
||||
status = "online"
|
||||
except Exception:
|
||||
status = "offline"
|
||||
time.sleep(self.check_interval)
|
||||
```
|
||||
|
||||
参考:`unilabos/devices/workstation/bioyond_studio/station.py`(`ConnectionMonitor`)
|
||||
|
||||
---
|
||||
|
||||
## 2. Config 结构模式
|
||||
|
||||
工作站的 `config` 在图文件中定义,传入 `__init__`。以下是常见字段模式:
|
||||
|
||||
### 2.1 外部系统连接
|
||||
|
||||
```json
|
||||
{
|
||||
"api_host": "http://192.168.1.100:8080",
|
||||
"api_key": "YOUR_API_KEY"
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 HTTP 回调服务
|
||||
|
||||
```json
|
||||
{
|
||||
"http_service_config": {
|
||||
"http_service_host": "127.0.0.1",
|
||||
"http_service_port": 8080
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 物料类型映射
|
||||
|
||||
将 PLR 资源类名映射到外部系统的物料类型(名称 + UUID)。用于双向物料转换。
|
||||
|
||||
```json
|
||||
{
|
||||
"material_type_mappings": {
|
||||
"PLR_ResourceClassName": ["外部系统显示名", "external-type-uuid"],
|
||||
"BIOYOND_PolymerStation_Reactor": ["反应器", "3a14233b-902d-0d7b-..."]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 仓库映射
|
||||
|
||||
将仓库名映射到外部系统的仓库 UUID 和库位 UUID。用于入库/出库操作。
|
||||
|
||||
```json
|
||||
{
|
||||
"warehouse_mapping": {
|
||||
"仓库名": {
|
||||
"uuid": "warehouse-uuid",
|
||||
"site_uuids": {
|
||||
"A01": "site-uuid-A01",
|
||||
"A02": "site-uuid-A02"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.5 工作流映射
|
||||
|
||||
将内部工作流名映射到外部系统的工作流 ID。
|
||||
|
||||
```json
|
||||
{
|
||||
"workflow_mappings": {
|
||||
"internal_workflow_name": "external-workflow-uuid"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.6 物料默认参数
|
||||
|
||||
```json
|
||||
{
|
||||
"material_default_parameters": {
|
||||
"NMP": {
|
||||
"unit": "毫升",
|
||||
"density": "1.03",
|
||||
"densityUnit": "g/mL",
|
||||
"description": "N-甲基吡咯烷酮"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 资源同步机制
|
||||
|
||||
### 3.1 ResourceSynchronizer
|
||||
|
||||
抽象基类,用于与外部物料系统双向同步。定义在 `workstation_base.py`。
|
||||
|
||||
```python
|
||||
from unilabos.devices.workstation.workstation_base import ResourceSynchronizer
|
||||
|
||||
|
||||
class MyResourceSynchronizer(ResourceSynchronizer):
|
||||
def __init__(self, workstation, api_client):
|
||||
super().__init__(workstation)
|
||||
self.api_client = api_client
|
||||
|
||||
def sync_from_external(self) -> bool:
|
||||
"""从外部系统拉取物料到 deck"""
|
||||
external_materials = self.api_client.list_materials()
|
||||
for material in external_materials:
|
||||
plr_resource = self._convert_to_plr(material)
|
||||
self.workstation.deck.assign_child_resource(plr_resource, coordinate)
|
||||
return True
|
||||
|
||||
def sync_to_external(self, plr_resource) -> bool:
|
||||
"""将 deck 中的物料变更推送到外部系统"""
|
||||
external_data = self._convert_from_plr(plr_resource)
|
||||
self.api_client.update_material(external_data)
|
||||
return True
|
||||
|
||||
def handle_external_change(self, change_info) -> bool:
|
||||
"""处理外部系统推送的物料变更"""
|
||||
return True
|
||||
```
|
||||
|
||||
### 3.2 update_resource — 上传资源树到云端
|
||||
|
||||
将 PLR Deck 序列化后通过 ROS 服务上传。典型使用场景:
|
||||
|
||||
```python
|
||||
# 在 post_init 中上传初始 deck
|
||||
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode
|
||||
|
||||
ROS2DeviceNode.run_async_func(
|
||||
self._ros_node.update_resource, True,
|
||||
**{"resources": [self.deck]}
|
||||
)
|
||||
|
||||
# 在动作方法中更新特定资源
|
||||
ROS2DeviceNode.run_async_func(
|
||||
self._ros_node.update_resource, True,
|
||||
**{"resources": [updated_plate]}
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 工作流序列管理
|
||||
|
||||
工作站通过 `workflow_sequence` 属性管理任务队列(JSON 字符串形式)。
|
||||
|
||||
```python
|
||||
class MyWorkstation(WorkstationBase):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._workflow_sequence = []
|
||||
|
||||
@property
|
||||
def workflow_sequence(self) -> str:
|
||||
"""返回 JSON 字符串,ROS 自动发布"""
|
||||
import json
|
||||
return json.dumps(self._workflow_sequence)
|
||||
|
||||
async def append_to_workflow_sequence(self, workflow_name: str) -> Dict[str, Any]:
|
||||
"""添加工作流到队列"""
|
||||
self._workflow_sequence.append({
|
||||
"name": workflow_name,
|
||||
"status": "pending",
|
||||
"created_at": time.time(),
|
||||
})
|
||||
return {"success": True}
|
||||
|
||||
async def clear_workflows(self) -> Dict[str, Any]:
|
||||
"""清空工作流队列"""
|
||||
self._workflow_sequence = []
|
||||
return {"success": True}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 站间物料转移
|
||||
|
||||
工作站之间转移物料的模式。通过 ROS ActionClient 调用目标站的动作。
|
||||
|
||||
```python
|
||||
async def transfer_materials_to_another_station(
|
||||
self,
|
||||
target_device_id: str,
|
||||
transfer_groups: list,
|
||||
**kwargs,
|
||||
) -> Dict[str, Any]:
|
||||
"""将物料转移到另一个工作站"""
|
||||
target_node = self._children.get(target_device_id)
|
||||
if not target_node:
|
||||
# 通过 ROS 节点查找非子设备的目标站
|
||||
pass
|
||||
|
||||
for group in transfer_groups:
|
||||
resource = self.find_resource_by_name(group["resource_name"])
|
||||
# 从本站 deck 移除
|
||||
resource.unassign()
|
||||
# 调用目标站的接收方法
|
||||
# ...
|
||||
|
||||
return {"success": True, "transferred": len(transfer_groups)}
|
||||
```
|
||||
|
||||
参考:`BioyondDispensingStation.transfer_materials_to_reaction_station`
|
||||
|
||||
---
|
||||
|
||||
## 6. post_init 完整模式
|
||||
|
||||
`post_init` 是工作站初始化的关键阶段,此时 ROS 节点和子设备已就绪。
|
||||
|
||||
```python
|
||||
def post_init(self, ros_node):
|
||||
super().post_init(ros_node)
|
||||
|
||||
# 1. 初始化外部系统客户端(此时 config 已可用)
|
||||
self.rpc_client = MySystemRPC(
|
||||
host=self.config.get("api_host"),
|
||||
api_key=self.config.get("api_key"),
|
||||
)
|
||||
self.hardware_interface = self.rpc_client
|
||||
|
||||
# 2. 启动连接监控
|
||||
self.connection_monitor = ConnectionMonitor(self)
|
||||
self.connection_monitor.start()
|
||||
|
||||
# 3. 启动 HTTP 回调服务
|
||||
if hasattr(self, '_http_service_config'):
|
||||
self.http_service = WorkstationHTTPService(
|
||||
workstation_instance=self,
|
||||
host=self._http_service_config["host"],
|
||||
port=self._http_service_config["port"],
|
||||
)
|
||||
self.http_service.start()
|
||||
|
||||
# 4. 上传 deck 到云端
|
||||
ROS2DeviceNode.run_async_func(
|
||||
self._ros_node.update_resource, True,
|
||||
**{"resources": [self.deck]}
|
||||
)
|
||||
|
||||
# 5. 初始化资源同步器(可选)
|
||||
self.resource_synchronizer = MyResourceSynchronizer(self, self.rpc_client)
|
||||
```
|
||||
261
.cursor/skills/batch-insert-reagent/SKILL.md
Normal file
261
.cursor/skills/batch-insert-reagent/SKILL.md
Normal file
@@ -0,0 +1,261 @@
|
||||
---
|
||||
name: batch-insert-reagent
|
||||
description: Batch insert reagents into Uni-Lab platform — add chemicals with CAS, SMILES, supplier info. Use when the user wants to add reagents, insert chemicals, batch register reagents, or mentions 录入试剂/添加试剂/试剂入库/reagent.
|
||||
---
|
||||
|
||||
# 批量录入试剂 Skill
|
||||
|
||||
通过云端 API 批量录入试剂信息,支持逐条或批量操作。
|
||||
|
||||
## 前置条件(缺一不可)
|
||||
|
||||
使用本 skill 前,**必须**先确认以下信息。如果缺少任何一项,**立即向用户询问并终止**,等补齐后再继续。
|
||||
|
||||
### 1. ak / sk → AUTH
|
||||
|
||||
询问用户的启动参数,从 `--ak` `--sk` 或 config.py 中获取。
|
||||
|
||||
生成 AUTH token(任选一种方式):
|
||||
|
||||
```bash
|
||||
# 方式一:Python 一行生成
|
||||
python -c "import base64,sys; print('Authorization: Lab ' + base64.b64encode(f'{sys.argv[1]}:{sys.argv[2]}'.encode()).decode())" <ak> <sk>
|
||||
|
||||
# 方式二:手动计算
|
||||
# base64(ak:sk) → Authorization: Lab <token>
|
||||
```
|
||||
|
||||
### 2. --addr → BASE URL
|
||||
|
||||
| `--addr` 值 | BASE |
|
||||
| ------------ | ----------------------------------- |
|
||||
| `test` | `https://leap-lab.test.bohrium.com` |
|
||||
| `uat` | `https://leap-lab.uat.bohrium.com` |
|
||||
| `local` | `http://127.0.0.1:48197` |
|
||||
| 不传(默认) | `https://leap-lab.bohrium.com` |
|
||||
|
||||
确认后设置:
|
||||
|
||||
```bash
|
||||
BASE="<根据 addr 确定的 URL>"
|
||||
AUTH="Authorization: Lab <gen_auth.py 输出的 token>"
|
||||
```
|
||||
|
||||
**两项全部就绪后才可发起 API 请求。**
|
||||
|
||||
## Session State
|
||||
|
||||
- `lab_uuid` — 实验室 UUID(首次通过 API #1 自动获取,**不需要问用户**)
|
||||
|
||||
## 请求约定
|
||||
|
||||
所有请求使用 `curl -s`,POST 需加 `Content-Type: application/json`。
|
||||
|
||||
> **Windows 平台**必须使用 `curl.exe`(而非 PowerShell 的 `curl` 别名),示例中的 `curl` 均指 `curl.exe`。
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### 1. 获取实验室信息(自动获取 lab_uuid)
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/edge/lab/info" -H "$AUTH"
|
||||
```
|
||||
|
||||
返回:
|
||||
|
||||
```json
|
||||
{ "code": 0, "data": { "uuid": "xxx", "name": "实验室名称" } }
|
||||
```
|
||||
|
||||
记住 `data.uuid` 为 `lab_uuid`。
|
||||
|
||||
### 2. 录入试剂
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/reagent" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"lab_uuid": "<lab_uuid>",
|
||||
"cas": "<CAS号>",
|
||||
"name": "<试剂名称>",
|
||||
"molecular_formula": "<分子式>",
|
||||
"smiles": "<SMILES>",
|
||||
"stock_in_quantity": <入库数量>,
|
||||
"unit": "<单位字符串>",
|
||||
"supplier": "<供应商>",
|
||||
"production_date": "<生产日期 ISO 8601>",
|
||||
"expiry_date": "<过期日期 ISO 8601>"
|
||||
}'
|
||||
```
|
||||
|
||||
返回成功时包含试剂 UUID:
|
||||
|
||||
```json
|
||||
{"code": 0, "data": {"uuid": "xxx", ...}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 试剂字段说明
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 | 示例 |
|
||||
| ------------------- | ------ | ---- | ----------------------------- | ------------------------ |
|
||||
| `lab_uuid` | string | 是 | 实验室 UUID(从 API #1 获取) | `"8511c672-..."` |
|
||||
| `cas` | string | 是 | CAS 注册号 | `"7732-18-3"` |
|
||||
| `name` | string | 是 | 试剂中文/英文名称 | `"水"` |
|
||||
| `molecular_formula` | string | 是 | 分子式 | `"H2O"` |
|
||||
| `smiles` | string | 是 | SMILES 表示 | `"O"` |
|
||||
| `stock_in_quantity` | number | 是 | 入库数量 | `10` |
|
||||
| `unit` | string | 是 | 单位(字符串,见下表) | `"mL"` |
|
||||
| `supplier` | string | 否 | 供应商名称 | `"国药集团"` |
|
||||
| `production_date` | string | 否 | 生产日期(ISO 8601) | `"2025-11-18T00:00:00Z"` |
|
||||
| `expiry_date` | string | 否 | 过期日期(ISO 8601) | `"2026-11-18T00:00:00Z"` |
|
||||
|
||||
### unit 单位值
|
||||
|
||||
| 值 | 单位 |
|
||||
| ------ | ---- |
|
||||
| `"mL"` | 毫升 |
|
||||
| `"L"` | 升 |
|
||||
| `"g"` | 克 |
|
||||
| `"kg"` | 千克 |
|
||||
| `"瓶"` | 瓶 |
|
||||
|
||||
> 根据试剂状态选择:液体用 `"mL"` / `"L"`,固体用 `"g"` / `"kg"`。
|
||||
|
||||
---
|
||||
|
||||
## 批量录入策略
|
||||
|
||||
### 方式一:用户提供 JSON 数组
|
||||
|
||||
用户一次性给出多条试剂数据:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"cas": "7732-18-3",
|
||||
"name": "水",
|
||||
"molecular_formula": "H2O",
|
||||
"smiles": "O",
|
||||
"stock_in_quantity": 10,
|
||||
"unit": "mL"
|
||||
},
|
||||
{
|
||||
"cas": "64-17-5",
|
||||
"name": "乙醇",
|
||||
"molecular_formula": "C2H6O",
|
||||
"smiles": "CCO",
|
||||
"stock_in_quantity": 5,
|
||||
"unit": "L"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Agent 自动为每条补充 `lab_uuid`、`production_date`、`expiry_date` 等字段后逐条提交。
|
||||
|
||||
Agent 循环调用 API #2 逐条录入,每条记录一次 API 调用。
|
||||
|
||||
### 方式二:用户逐个描述
|
||||
|
||||
用户口头描述试剂(如「帮我录入 500mL 的无水乙醇,Sigma 的」),agent 自行补全字段:
|
||||
|
||||
1. 根据名称查找 CAS 号、分子式、SMILES(参考下方速查表或自行推断)
|
||||
2. 构建完整的请求体
|
||||
3. 向用户确认后提交
|
||||
|
||||
### 方式三:从 CSV/表格批量导入
|
||||
|
||||
用户提供 CSV 或表格文件路径,agent 读取并解析:
|
||||
|
||||
```bash
|
||||
# 期望的 CSV 格式(首行为表头)
|
||||
cas,name,molecular_formula,smiles,stock_in_quantity,unit,supplier,production_date,expiry_date
|
||||
7732-18-3,水,H2O,O,10,mL,农夫山泉,2025-11-18T00:00:00Z,2026-11-18T00:00:00Z
|
||||
```
|
||||
|
||||
### 日期格式规则(重要)
|
||||
|
||||
所有日期字段(`production_date`、`expiry_date`)**必须**使用 ISO 8601 完整格式:`YYYY-MM-DDTHH:MM:SSZ`。
|
||||
|
||||
- 用户输入 `2025-03-01` → 转换为 `"2025-03-01T00:00:00Z"`
|
||||
- 用户输入 `2025/9/1` → 转换为 `"2025-09-01T00:00:00Z"`
|
||||
- 用户未提供日期 → 使用当天日期 + `T00:00:00Z`,有效期默认 +1 年
|
||||
|
||||
**禁止**发送不带时间部分的日期字符串(如 `"2025-03-01"`),API 会拒绝。
|
||||
|
||||
### 执行与汇报
|
||||
|
||||
每次 API 调用后:
|
||||
|
||||
1. 检查返回 `code`(0 = 成功)
|
||||
2. 记录成功/失败数量
|
||||
3. 全部完成后汇总:「共录入 N 条试剂,成功 X 条,失败 Y 条」
|
||||
4. 如有失败,列出失败的试剂名称和错误信息
|
||||
|
||||
---
|
||||
|
||||
## 常见试剂速查表
|
||||
|
||||
| 名称 | CAS | 分子式 | SMILES |
|
||||
| --------------------- | --------- | ---------- | ------------------------------------ |
|
||||
| 水 | 7732-18-3 | H2O | O |
|
||||
| 乙醇 | 64-17-5 | C2H6O | CCO |
|
||||
| 乙酸 | 64-19-7 | C2H4O2 | CC(O)=O |
|
||||
| 甲醇 | 67-56-1 | CH4O | CO |
|
||||
| 丙酮 | 67-64-1 | C3H6O | CC(C)=O |
|
||||
| 二甲基亚砜(DMSO) | 67-68-5 | C2H6OS | CS(C)=O |
|
||||
| 乙酸乙酯 | 141-78-6 | C4H8O2 | CCOC(C)=O |
|
||||
| 二氯甲烷 | 75-09-2 | CH2Cl2 | ClCCl |
|
||||
| 四氢呋喃(THF) | 109-99-9 | C4H8O | C1CCOC1 |
|
||||
| N,N-二甲基甲酰胺(DMF) | 68-12-2 | C3H7NO | CN(C)C=O |
|
||||
| 氯仿 | 67-66-3 | CHCl3 | ClC(Cl)Cl |
|
||||
| 乙腈 | 75-05-8 | C2H3N | CC#N |
|
||||
| 甲苯 | 108-88-3 | C7H8 | Cc1ccccc1 |
|
||||
| 正己烷 | 110-54-3 | C6H14 | CCCCCC |
|
||||
| 异丙醇 | 67-63-0 | C3H8O | CC(C)O |
|
||||
| 盐酸 | 7647-01-0 | HCl | Cl |
|
||||
| 硫酸 | 7664-93-9 | H2SO4 | OS(O)(=O)=O |
|
||||
| 氢氧化钠 | 1310-73-2 | NaOH | [Na]O |
|
||||
| 碳酸钠 | 497-19-8 | Na2CO3 | [Na]OC([O-])=O.[Na+] |
|
||||
| 氯化钠 | 7647-14-5 | NaCl | [Na]Cl |
|
||||
| 乙二胺四乙酸(EDTA) | 60-00-4 | C10H16N2O8 | OC(=O)CN(CCN(CC(O)=O)CC(O)=O)CC(O)=O |
|
||||
|
||||
> 此表仅供快速参考。对于不在表中的试剂,agent 应根据化学知识推断或提示用户补充。
|
||||
|
||||
---
|
||||
|
||||
## 完整工作流 Checklist
|
||||
|
||||
```
|
||||
Task Progress:
|
||||
- [ ] Step 1: 确认 ak/sk → 生成 AUTH token
|
||||
- [ ] Step 2: 确认 --addr → 设置 BASE URL
|
||||
- [ ] Step 3: GET /edge/lab/info → 获取 lab_uuid
|
||||
- [ ] Step 4: 收集试剂信息(用户提供列表/逐个描述/CSV文件)
|
||||
- [ ] Step 5: 补全缺失字段(CAS、分子式、SMILES 等)
|
||||
- [ ] Step 6: 向用户确认待录入的试剂列表
|
||||
- [ ] Step 7: 循环调用 POST /lab/reagent 逐条录入(每条需含 lab_uuid)
|
||||
- [ ] Step 8: 汇总结果(成功/失败数量及详情)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 完整示例
|
||||
|
||||
用户说:「帮我录入 3 种试剂:500mL 无水乙醇、1kg 氯化钠、2L 去离子水」
|
||||
|
||||
Agent 构建的请求序列:
|
||||
|
||||
```json
|
||||
// 第 1 条
|
||||
{"lab_uuid": "8511c672-...", "cas": "64-17-5", "name": "无水乙醇", "molecular_formula": "C2H6O", "smiles": "CCO", "stock_in_quantity": 500, "unit": "mL", "supplier": "国药集团", "production_date": "2025-01-01T00:00:00Z", "expiry_date": "2026-01-01T00:00:00Z"}
|
||||
|
||||
// 第 2 条
|
||||
{"lab_uuid": "8511c672-...", "cas": "7647-14-5", "name": "氯化钠", "molecular_formula": "NaCl", "smiles": "[Na]Cl", "stock_in_quantity": 1, "unit": "kg", "supplier": "", "production_date": "2025-01-01T00:00:00Z", "expiry_date": "2026-01-01T00:00:00Z"}
|
||||
|
||||
// 第 3 条
|
||||
{"lab_uuid": "8511c672-...", "cas": "7732-18-3", "name": "去离子水", "molecular_formula": "H2O", "smiles": "O", "stock_in_quantity": 2, "unit": "L", "supplier": "", "production_date": "2025-01-01T00:00:00Z", "expiry_date": "2026-01-01T00:00:00Z"}
|
||||
```
|
||||
360
.cursor/skills/batch-submit-experiment/SKILL.md
Normal file
360
.cursor/skills/batch-submit-experiment/SKILL.md
Normal file
@@ -0,0 +1,360 @@
|
||||
---
|
||||
name: batch-submit-experiment
|
||||
description: Batch submit experiments (notebooks) to the Uni-Lab cloud platform (leap-lab) — list workflows, generate node_params from registry schemas, submit multiple rounds, check notebook status. Use when the user wants to submit experiments, create notebooks, batch run workflows, check experiment status, or mentions 提交实验/批量实验/notebook/实验轮次/实验状态.
|
||||
---
|
||||
|
||||
# Uni-Lab 批量提交实验指南
|
||||
|
||||
通过 Uni-Lab 云端 API 批量提交实验(notebook),支持多轮实验参数配置。根据 workflow 模板详情和本地设备注册表自动生成 `node_params` 模板。
|
||||
|
||||
> **重要**:本指南中的 `Authorization: Lab <token>` 是 **Uni-Lab 平台专用的认证方式**,`Lab` 是 Uni-Lab 的 auth scheme 关键字,**不是** HTTP Basic 认证。请勿将其替换为 `Basic`。
|
||||
|
||||
## 前置条件(缺一不可)
|
||||
|
||||
使用本指南前,**必须**先确认以下信息。如果缺少任何一项,**立即向用户询问并终止**,等补齐后再继续。
|
||||
|
||||
### 1. ak / sk → AUTH
|
||||
|
||||
询问用户的启动参数,从 `--ak` `--sk` 或 config.py 中获取。
|
||||
|
||||
生成 AUTH token(任选一种方式):
|
||||
|
||||
```bash
|
||||
# 方式一:Python 一行生成(注意:scheme 是 "Lab" 不是 "Basic")
|
||||
python -c "import base64,sys; print('Authorization: Lab ' + base64.b64encode(f'{sys.argv[1]}:{sys.argv[2]}'.encode()).decode())" <ak> <sk>
|
||||
|
||||
# 方式二:手动计算
|
||||
# base64(ak:sk) → Authorization: Lab <token>
|
||||
# ⚠️ 这里的 "Lab" 是 Uni-Lab 平台的 auth scheme,绝对不能用 "Basic" 替代
|
||||
```
|
||||
|
||||
### 2. --addr → BASE URL
|
||||
|
||||
| `--addr` 值 | BASE |
|
||||
| ------------ | ----------------------------------- |
|
||||
| `test` | `https://leap-lab.test.bohrium.com` |
|
||||
| `uat` | `https://leap-lab.uat.bohrium.com` |
|
||||
| `local` | `http://127.0.0.1:48197` |
|
||||
| 不传(默认) | `https://leap-lab.bohrium.com` |
|
||||
|
||||
确认后设置:
|
||||
|
||||
```bash
|
||||
BASE="<根据 addr 确定的 URL>"
|
||||
# ⚠️ Auth scheme 必须是 "Lab"(Uni-Lab 专用),不是 "Basic"
|
||||
AUTH="Authorization: Lab <上面命令输出的 token>"
|
||||
```
|
||||
|
||||
### 3. req_device_registry_upload.json(设备注册表)
|
||||
|
||||
**批量提交实验时需要本地注册表来解析 workflow 节点的参数 schema。**
|
||||
|
||||
**必须先用 Glob 工具搜索文件**,不要直接猜测路径:
|
||||
|
||||
```
|
||||
Glob: **/req_device_registry_upload.json
|
||||
```
|
||||
|
||||
常见位置(仅供参考,以 Glob 实际结果为准):
|
||||
- `<workspace>/unilabos_data/req_device_registry_upload.json`
|
||||
- `<workspace>/req_device_registry_upload.json`
|
||||
|
||||
找到后**检查文件修改时间**并告知用户。超过 1 天提醒用户是否需要重新启动 `unilab`。
|
||||
|
||||
**如果 Glob 搜索无结果** → 告知用户先运行 `unilab` 启动命令,等注册表生成后再执行。可跳过此步,但将无法自动生成参数模板,需要用户手动填写 `param`。
|
||||
|
||||
### 4. workflow_uuid(目标工作流)
|
||||
|
||||
用户需要提供要提交的 workflow UUID。如果用户不确定,通过 API #3 列出可用 workflow 供选择。
|
||||
|
||||
**四项全部就绪后才可开始。**
|
||||
|
||||
## Session State
|
||||
|
||||
在整个对话过程中,agent 需要记住以下状态,避免重复询问用户:
|
||||
|
||||
- `lab_uuid` — 实验室 UUID(首次通过 API #1 自动获取,**不需要问用户**)
|
||||
- `project_uuid` — 项目 UUID(通过 API #2 列出项目列表,**让用户选择**)
|
||||
- `workflow_uuid` — 工作流 UUID(用户提供或从列表选择)
|
||||
- `workflow_nodes` — workflow 中各 action 节点的 uuid、设备 ID、动作名(从 API #4 获取)
|
||||
|
||||
## 请求约定
|
||||
|
||||
所有请求使用 `curl -s`,POST 需加 `Content-Type: application/json`。
|
||||
|
||||
> **Windows 平台**必须使用 `curl.exe`(而非 PowerShell 的 `curl` 别名),示例中的 `curl` 均指 `curl.exe`。
|
||||
>
|
||||
> **PowerShell JSON 传参**:PowerShell 中 `-d '{"key":"value"}'` 会因引号转义失败。请将 JSON 写入临时文件,用 `-d '@tmp_body.json'`(单引号包裹 `@`,否则会被解析为 splatting 运算符)。
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### 1. 获取实验室信息(自动获取 lab_uuid)
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/edge/lab/info" -H "$AUTH"
|
||||
```
|
||||
|
||||
返回:
|
||||
|
||||
```json
|
||||
{ "code": 0, "data": { "uuid": "xxx", "name": "实验室名称" } }
|
||||
```
|
||||
|
||||
记住 `data.uuid` 为 `lab_uuid`。
|
||||
|
||||
### 2. 列出实验室项目(让用户选择项目)
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/project/list?lab_uuid=$lab_uuid" -H "$AUTH"
|
||||
```
|
||||
|
||||
返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"uuid": "1b3f249a-...",
|
||||
"name": "bt",
|
||||
"description": null,
|
||||
"status": "active",
|
||||
"created_at": "2026-04-09T14:31:28+08:00"
|
||||
},
|
||||
{
|
||||
"uuid": "b6366243-...",
|
||||
"name": "default",
|
||||
"description": "默认项目",
|
||||
"status": "active",
|
||||
"created_at": "2026-03-26T11:13:36+08:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
展示 `data.items[]` 中每个项目的 `name` 和 `uuid`,让用户选择。用户**必须**选择一个项目,记住 `project_uuid`(即选中项目的 `uuid`),后续创建 notebook 时需要提供。
|
||||
|
||||
### 3. 列出可用 workflow
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/workflow/workflows?page=1&page_size=20&lab_uuid=$lab_uuid" -H "$AUTH"
|
||||
```
|
||||
|
||||
返回 workflow 列表,展示给用户选择。列出每个 workflow 的 `uuid` 和 `name`。
|
||||
|
||||
### 4. 获取 workflow 模板详情
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/workflow/template/detail/$workflow_uuid" -H "$AUTH"
|
||||
```
|
||||
|
||||
返回 workflow 的完整结构,包含所有 action 节点信息。需要从响应中提取:
|
||||
|
||||
- 每个 action 节点的 `node_uuid`
|
||||
- 每个节点对应的设备 ID(`resource_template_name`)
|
||||
- 每个节点的动作名(`node_template_name`)
|
||||
- 每个节点的现有参数(`param`)
|
||||
|
||||
> **注意**:此 API 返回格式可能因版本不同而有差异。首次调用时,先打印完整响应分析结构,再提取节点信息。常见的节点字段路径为 `data.nodes[]` 或 `data.workflow_nodes[]`。
|
||||
|
||||
### 5. 提交实验(创建 notebook)
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/notebook" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '<request_body>'
|
||||
```
|
||||
|
||||
请求体结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"lab_uuid": "<lab_uuid>",
|
||||
"project_uuid": "<project_uuid>",
|
||||
"workflow_uuid": "<workflow_uuid>",
|
||||
"name": "<实验名称>",
|
||||
"node_params": [
|
||||
{
|
||||
"sample_uuids": ["<样品UUID1>", "<样品UUID2>"],
|
||||
"datas": [
|
||||
{
|
||||
"node_uuid": "<workflow中的节点UUID>",
|
||||
"param": {},
|
||||
"sample_params": [
|
||||
{
|
||||
"container_uuid": "<容器UUID>",
|
||||
"sample_value": {
|
||||
"liquid_names": "<液体名称>",
|
||||
"volumes": 1000
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
> **注意**:`sample_uuids` 必须是 **UUID 数组**(`[]uuid.UUID`),不是字符串。无样品时传空数组 `[]`。
|
||||
|
||||
### 6. 查询 notebook 状态
|
||||
|
||||
提交成功后,使用返回的 notebook UUID 查询执行状态:
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/notebook/status?uuid=$notebook_uuid" -H "$AUTH"
|
||||
```
|
||||
|
||||
提交后应**立即查询一次**状态,确认 notebook 已被正确接收并开始调度。
|
||||
|
||||
---
|
||||
|
||||
## Notebook 请求体详解
|
||||
|
||||
### node_params 结构
|
||||
|
||||
`node_params` 是一个数组,**每个元素代表一轮实验**:
|
||||
|
||||
- 要跑 2 轮 → `node_params` 有 2 个元素
|
||||
- 要跑 N 轮 → `node_params` 有 N 个元素
|
||||
|
||||
### 每轮的字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| -------------- | ------------- | ----------------------------------------- |
|
||||
| `sample_uuids` | array\<uuid\> | 该轮实验的样品 UUID 数组,无样品时传 `[]` |
|
||||
| `datas` | array | 该轮中每个 workflow 节点的参数配置 |
|
||||
|
||||
### datas 中每个节点
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --------------- | ------ | -------------------------------------------- |
|
||||
| `node_uuid` | string | workflow 模板中的节点 UUID(从 API #4 获取) |
|
||||
| `param` | object | 动作参数(根据本地注册表 schema 填写) |
|
||||
| `sample_params` | array | 样品相关参数(液体名、体积等) |
|
||||
|
||||
### sample_params 中每条
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ---------------- | ------ | ---------------------------------------------------- |
|
||||
| `container_uuid` | string | 容器 UUID |
|
||||
| `sample_value` | object | 样品值,如 `{"liquid_names": "水", "volumes": 1000}` |
|
||||
|
||||
---
|
||||
|
||||
## 从本地注册表生成 param 模板
|
||||
|
||||
### 自动方式 — 运行脚本
|
||||
|
||||
```bash
|
||||
python scripts/gen_notebook_params.py \
|
||||
--auth <token> \
|
||||
--base <BASE_URL> \
|
||||
--workflow-uuid <workflow_uuid> \
|
||||
[--registry <path/to/req_device_registry_upload.json>] \
|
||||
[--rounds <轮次数>] \
|
||||
[--output <输出文件路径>]
|
||||
```
|
||||
|
||||
> 脚本位于本文档同级目录下的 `scripts/gen_notebook_params.py`。
|
||||
|
||||
脚本会:
|
||||
|
||||
1. 调用 workflow detail API 获取所有 action 节点
|
||||
2. 读取本地注册表,为每个节点查找对应的 action schema
|
||||
3. 生成 `notebook_template.json`,包含:
|
||||
- 完整 `node_params` 骨架
|
||||
- 每个节点的 param 字段及类型说明
|
||||
- `_schema_info` 辅助信息(不提交,仅供参考)
|
||||
|
||||
### 手动方式
|
||||
|
||||
如果脚本不可用或注册表不存在:
|
||||
|
||||
1. 调用 API #4 获取 workflow 详情
|
||||
2. 找到每个 action 节点的 `node_uuid`
|
||||
3. 在本地注册表中查找对应设备的 `action_value_mappings`:
|
||||
```
|
||||
resources[].id == <device_id>
|
||||
→ resources[].class.action_value_mappings.<action_name>.schema.properties.goal.properties
|
||||
```
|
||||
4. 将 schema 中的 properties 作为 `param` 的字段模板
|
||||
5. 按轮次复制 `node_params` 元素,让用户填写每轮的具体值
|
||||
|
||||
### 注册表结构参考
|
||||
|
||||
```json
|
||||
{
|
||||
"resources": [
|
||||
{
|
||||
"id": "liquid_handler.prcxi",
|
||||
"class": {
|
||||
"module": "unilabos.devices.xxx:ClassName",
|
||||
"action_value_mappings": {
|
||||
"transfer_liquid": {
|
||||
"type": "LiquidHandlerTransfer",
|
||||
"schema": {
|
||||
"properties": {
|
||||
"goal": {
|
||||
"properties": {
|
||||
"asp_vols": {
|
||||
"type": "array",
|
||||
"items": { "type": "number" }
|
||||
},
|
||||
"sources": { "type": "array" }
|
||||
},
|
||||
"required": ["asp_vols", "sources"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"goal_default": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`param` 填写时,使用 `goal.properties` 中的字段名和类型。
|
||||
|
||||
---
|
||||
|
||||
## 完整工作流 Checklist
|
||||
|
||||
```
|
||||
Task Progress:
|
||||
- [ ] Step 1: 确认 ak/sk → 生成 AUTH token
|
||||
- [ ] Step 2: 确认 --addr → 设置 BASE URL
|
||||
- [ ] Step 3: GET /edge/lab/info → 获取 lab_uuid
|
||||
- [ ] Step 4: GET /lab/project/list → 列出项目,让用户选择 → 获取 project_uuid
|
||||
- [ ] Step 5: 确认 workflow_uuid(用户提供或从 GET #3 列表选择)
|
||||
- [ ] Step 6: GET workflow detail (#4) → 提取各节点 uuid、设备ID、动作名
|
||||
- [ ] Step 7: 定位本地注册表 req_device_registry_upload.json
|
||||
- [ ] Step 8: 运行 gen_notebook_params.py 或手动匹配 → 生成 node_params 模板
|
||||
- [ ] Step 9: 引导用户填写每轮的参数(sample_uuids、param、sample_params)
|
||||
- [ ] Step 10: 构建完整请求体(含 project_uuid)→ POST /lab/notebook 提交
|
||||
- [ ] Step 11: 检查返回结果,记录 notebook UUID
|
||||
- [ ] Step 12: GET /lab/notebook/status → 查询 notebook 状态,确认已调度
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: workflow 中有多个节点,每轮都要填所有节点的参数吗?
|
||||
|
||||
是的。`datas` 数组中需要包含该轮实验涉及的每个 workflow 节点的参数。通常每个 action 节点都需要一条 `datas` 记录。
|
||||
|
||||
### Q: 多轮实验的参数完全不同吗?
|
||||
|
||||
通常每轮的 `param`(设备动作参数)可能相同或相似,但 `sample_uuids` 和 `sample_params`(样品信息)每轮不同。脚本生成模板时会按轮次复制骨架,用户只需修改差异部分。
|
||||
|
||||
### Q: 如何获取 sample_uuids 和 container_uuid?
|
||||
|
||||
这些 UUID 通常来自实验室的样品管理系统。向用户询问,或从资源树(API `GET /lab/material/download/$lab_uuid`)中查找。
|
||||
@@ -0,0 +1,395 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
从 workflow 模板详情 + 本地设备注册表生成 notebook 提交用的 node_params 模板。
|
||||
|
||||
用法:
|
||||
python gen_notebook_params.py --auth <token> --base <url> --workflow-uuid <uuid> [选项]
|
||||
|
||||
选项:
|
||||
--auth <token> Lab token(base64(ak:sk) 的结果,不含 "Lab " 前缀)
|
||||
--base <url> API 基础 URL(如 https://leap-lab.test.bohrium.com)
|
||||
--workflow-uuid <uuid> 目标 workflow 的 UUID
|
||||
--registry <path> 本地注册表文件路径(默认自动搜索)
|
||||
--rounds <n> 实验轮次数(默认 1)
|
||||
--output <path> 输出模板文件路径(默认 notebook_template.json)
|
||||
--dump-response 打印 workflow detail API 的原始响应(调试用)
|
||||
|
||||
示例:
|
||||
python gen_notebook_params.py \\
|
||||
--auth YTFmZDlkNGUtxxxx \\
|
||||
--base https://leap-lab.test.bohrium.com \\
|
||||
--workflow-uuid abc-123-def \\
|
||||
--rounds 2
|
||||
"""
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from urllib.request import Request, urlopen
|
||||
from urllib.error import HTTPError, URLError
|
||||
|
||||
REGISTRY_FILENAME = "req_device_registry_upload.json"
|
||||
|
||||
|
||||
def find_registry(explicit_path=None):
|
||||
"""查找本地注册表文件,逻辑同 extract_device_actions.py"""
|
||||
if explicit_path:
|
||||
if os.path.isfile(explicit_path):
|
||||
return explicit_path
|
||||
if os.path.isdir(explicit_path):
|
||||
fp = os.path.join(explicit_path, REGISTRY_FILENAME)
|
||||
if os.path.isfile(fp):
|
||||
return fp
|
||||
print(f"警告: 指定的注册表路径不存在: {explicit_path}")
|
||||
return None
|
||||
|
||||
candidates = [
|
||||
os.path.join("unilabos_data", REGISTRY_FILENAME),
|
||||
REGISTRY_FILENAME,
|
||||
]
|
||||
for c in candidates:
|
||||
if os.path.isfile(c):
|
||||
return c
|
||||
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
workspace_root = os.path.normpath(os.path.join(script_dir, "..", "..", ".."))
|
||||
for c in candidates:
|
||||
path = os.path.join(workspace_root, c)
|
||||
if os.path.isfile(path):
|
||||
return path
|
||||
|
||||
cwd = os.getcwd()
|
||||
for _ in range(5):
|
||||
parent = os.path.dirname(cwd)
|
||||
if parent == cwd:
|
||||
break
|
||||
cwd = parent
|
||||
for c in candidates:
|
||||
path = os.path.join(cwd, c)
|
||||
if os.path.isfile(path):
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def load_registry(path):
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def build_registry_index(registry_data):
|
||||
"""构建 device_id → action_value_mappings 的索引"""
|
||||
index = {}
|
||||
for res in registry_data.get("resources", []):
|
||||
rid = res.get("id", "")
|
||||
avm = res.get("class", {}).get("action_value_mappings", {})
|
||||
if rid and avm:
|
||||
index[rid] = avm
|
||||
return index
|
||||
|
||||
|
||||
def flatten_goal_schema(action_data):
|
||||
"""从 action_value_mappings 条目中提取 goal 层的 schema"""
|
||||
schema = action_data.get("schema", {})
|
||||
goal_schema = schema.get("properties", {}).get("goal", {})
|
||||
return goal_schema if goal_schema else schema
|
||||
|
||||
|
||||
def build_param_template(goal_schema):
|
||||
"""根据 goal schema 生成 param 模板,含类型标注"""
|
||||
properties = goal_schema.get("properties", {})
|
||||
required = set(goal_schema.get("required", []))
|
||||
template = {}
|
||||
for field_name, field_def in properties.items():
|
||||
if field_name == "unilabos_device_id":
|
||||
continue
|
||||
ftype = field_def.get("type", "any")
|
||||
default = field_def.get("default")
|
||||
if default is not None:
|
||||
template[field_name] = default
|
||||
elif ftype == "string":
|
||||
template[field_name] = f"$TODO ({ftype}, {'required' if field_name in required else 'optional'})"
|
||||
elif ftype == "number" or ftype == "integer":
|
||||
template[field_name] = 0
|
||||
elif ftype == "boolean":
|
||||
template[field_name] = False
|
||||
elif ftype == "array":
|
||||
template[field_name] = []
|
||||
elif ftype == "object":
|
||||
template[field_name] = {}
|
||||
else:
|
||||
template[field_name] = f"$TODO ({ftype})"
|
||||
return template
|
||||
|
||||
|
||||
def fetch_workflow_detail(base_url, auth_token, workflow_uuid):
|
||||
"""调用 workflow detail API"""
|
||||
url = f"{base_url}/api/v1/lab/workflow/template/detail/{workflow_uuid}"
|
||||
req = Request(url, method="GET")
|
||||
req.add_header("Authorization", f"Lab {auth_token}")
|
||||
try:
|
||||
with urlopen(req, timeout=30) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
except HTTPError as e:
|
||||
body = e.read().decode("utf-8", errors="replace")
|
||||
print(f"API 错误 {e.code}: {body}")
|
||||
return None
|
||||
except URLError as e:
|
||||
print(f"网络错误: {e.reason}")
|
||||
return None
|
||||
|
||||
|
||||
def extract_nodes_from_response(response):
|
||||
"""
|
||||
从 workflow detail 响应中提取 action 节点列表。
|
||||
适配多种可能的响应格式。
|
||||
|
||||
返回: [(node_uuid, resource_template_name, node_template_name, existing_param), ...]
|
||||
"""
|
||||
data = response.get("data", response)
|
||||
|
||||
search_keys = ["nodes", "workflow_nodes", "node_list", "steps"]
|
||||
nodes_raw = None
|
||||
for key in search_keys:
|
||||
if key in data and isinstance(data[key], list):
|
||||
nodes_raw = data[key]
|
||||
break
|
||||
|
||||
if nodes_raw is None:
|
||||
if isinstance(data, list):
|
||||
nodes_raw = data
|
||||
else:
|
||||
for v in data.values():
|
||||
if isinstance(v, list) and len(v) > 0 and isinstance(v[0], dict):
|
||||
nodes_raw = v
|
||||
break
|
||||
|
||||
if not nodes_raw:
|
||||
print("警告: 未能从响应中提取节点列表")
|
||||
print("响应顶层 keys:", list(data.keys()) if isinstance(data, dict) else type(data).__name__)
|
||||
return []
|
||||
|
||||
result = []
|
||||
for node in nodes_raw:
|
||||
if not isinstance(node, dict):
|
||||
continue
|
||||
|
||||
node_uuid = (
|
||||
node.get("uuid")
|
||||
or node.get("node_uuid")
|
||||
or node.get("id")
|
||||
or ""
|
||||
)
|
||||
resource_name = (
|
||||
node.get("resource_template_name")
|
||||
or node.get("device_id")
|
||||
or node.get("resource_name")
|
||||
or node.get("device_name")
|
||||
or ""
|
||||
)
|
||||
template_name = (
|
||||
node.get("node_template_name")
|
||||
or node.get("action_name")
|
||||
or node.get("template_name")
|
||||
or node.get("action")
|
||||
or node.get("name")
|
||||
or ""
|
||||
)
|
||||
existing_param = node.get("param", {}) or {}
|
||||
|
||||
if node_uuid:
|
||||
result.append((node_uuid, resource_name, template_name, existing_param))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def generate_template(nodes, registry_index, rounds):
|
||||
"""生成 notebook 提交模板"""
|
||||
node_params = []
|
||||
schema_info = {}
|
||||
|
||||
datas_template = []
|
||||
for node_uuid, resource_name, template_name, existing_param in nodes:
|
||||
param_template = {}
|
||||
matched = False
|
||||
|
||||
if resource_name and template_name and resource_name in registry_index:
|
||||
avm = registry_index[resource_name]
|
||||
if template_name in avm:
|
||||
goal_schema = flatten_goal_schema(avm[template_name])
|
||||
param_template = build_param_template(goal_schema)
|
||||
goal_default = avm[template_name].get("goal_default", {})
|
||||
if goal_default:
|
||||
for k, v in goal_default.items():
|
||||
if k in param_template and v is not None:
|
||||
param_template[k] = v
|
||||
matched = True
|
||||
|
||||
schema_info[node_uuid] = {
|
||||
"device_id": resource_name,
|
||||
"action_name": template_name,
|
||||
"action_type": avm[template_name].get("type", ""),
|
||||
"schema_properties": list(goal_schema.get("properties", {}).keys()),
|
||||
"required": goal_schema.get("required", []),
|
||||
}
|
||||
|
||||
if not matched and existing_param:
|
||||
param_template = existing_param
|
||||
|
||||
if not matched and not existing_param:
|
||||
schema_info[node_uuid] = {
|
||||
"device_id": resource_name,
|
||||
"action_name": template_name,
|
||||
"warning": "未在本地注册表中找到匹配的 action schema",
|
||||
}
|
||||
|
||||
datas_template.append({
|
||||
"node_uuid": node_uuid,
|
||||
"param": param_template,
|
||||
"sample_params": [
|
||||
{
|
||||
"container_uuid": "$TODO_CONTAINER_UUID",
|
||||
"sample_value": {
|
||||
"liquid_names": "$TODO_LIQUID_NAME",
|
||||
"volumes": 0,
|
||||
},
|
||||
}
|
||||
],
|
||||
})
|
||||
|
||||
for i in range(rounds):
|
||||
node_params.append({
|
||||
"sample_uuids": f"$TODO_SAMPLE_UUID_ROUND_{i + 1}",
|
||||
"datas": copy.deepcopy(datas_template),
|
||||
})
|
||||
|
||||
return {
|
||||
"lab_uuid": "$TODO_LAB_UUID",
|
||||
"project_uuid": "$TODO_PROJECT_UUID",
|
||||
"workflow_uuid": "$TODO_WORKFLOW_UUID",
|
||||
"name": "$TODO_EXPERIMENT_NAME",
|
||||
"node_params": node_params,
|
||||
"_schema_info(仅参考,提交时删除)": schema_info,
|
||||
}
|
||||
|
||||
|
||||
def parse_args(argv):
|
||||
"""简单的参数解析"""
|
||||
opts = {
|
||||
"auth": None,
|
||||
"base": None,
|
||||
"workflow_uuid": None,
|
||||
"registry": None,
|
||||
"rounds": 1,
|
||||
"output": "notebook_template.json",
|
||||
"dump_response": False,
|
||||
}
|
||||
i = 0
|
||||
while i < len(argv):
|
||||
arg = argv[i]
|
||||
if arg == "--auth" and i + 1 < len(argv):
|
||||
opts["auth"] = argv[i + 1]
|
||||
i += 2
|
||||
elif arg == "--base" and i + 1 < len(argv):
|
||||
opts["base"] = argv[i + 1].rstrip("/")
|
||||
i += 2
|
||||
elif arg == "--workflow-uuid" and i + 1 < len(argv):
|
||||
opts["workflow_uuid"] = argv[i + 1]
|
||||
i += 2
|
||||
elif arg == "--registry" and i + 1 < len(argv):
|
||||
opts["registry"] = argv[i + 1]
|
||||
i += 2
|
||||
elif arg == "--rounds" and i + 1 < len(argv):
|
||||
opts["rounds"] = int(argv[i + 1])
|
||||
i += 2
|
||||
elif arg == "--output" and i + 1 < len(argv):
|
||||
opts["output"] = argv[i + 1]
|
||||
i += 2
|
||||
elif arg == "--dump-response":
|
||||
opts["dump_response"] = True
|
||||
i += 1
|
||||
else:
|
||||
print(f"未知参数: {arg}")
|
||||
i += 1
|
||||
return opts
|
||||
|
||||
|
||||
def main():
|
||||
opts = parse_args(sys.argv[1:])
|
||||
|
||||
if not opts["auth"] or not opts["base"] or not opts["workflow_uuid"]:
|
||||
print("用法:")
|
||||
print(" python gen_notebook_params.py --auth <token> --base <url> --workflow-uuid <uuid> [选项]")
|
||||
print()
|
||||
print("必需参数:")
|
||||
print(" --auth <token> Lab token(base64(ak:sk))")
|
||||
print(" --base <url> API 基础 URL")
|
||||
print(" --workflow-uuid <uuid> 目标 workflow UUID")
|
||||
print()
|
||||
print("可选参数:")
|
||||
print(" --registry <path> 注册表文件路径(默认自动搜索)")
|
||||
print(" --rounds <n> 实验轮次数(默认 1)")
|
||||
print(" --output <path> 输出文件路径(默认 notebook_template.json)")
|
||||
print(" --dump-response 打印 API 原始响应")
|
||||
sys.exit(1)
|
||||
|
||||
# 1. 查找并加载本地注册表
|
||||
registry_path = find_registry(opts["registry"])
|
||||
registry_index = {}
|
||||
if registry_path:
|
||||
mtime = os.path.getmtime(registry_path)
|
||||
gen_time = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(f"注册表: {registry_path} (生成时间: {gen_time})")
|
||||
registry_data = load_registry(registry_path)
|
||||
registry_index = build_registry_index(registry_data)
|
||||
print(f"已索引 {len(registry_index)} 个设备的 action schemas")
|
||||
else:
|
||||
print("警告: 未找到本地注册表,将跳过 param 模板生成")
|
||||
print(" 提交时需要手动填写各节点的 param 字段")
|
||||
|
||||
# 2. 获取 workflow 详情
|
||||
print(f"\n正在获取 workflow 详情: {opts['workflow_uuid']}")
|
||||
response = fetch_workflow_detail(opts["base"], opts["auth"], opts["workflow_uuid"])
|
||||
if not response:
|
||||
print("错误: 无法获取 workflow 详情")
|
||||
sys.exit(1)
|
||||
|
||||
if opts["dump_response"]:
|
||||
print("\n=== API 原始响应 ===")
|
||||
print(json.dumps(response, indent=2, ensure_ascii=False)[:5000])
|
||||
print("=== 响应结束(截断至 5000 字符) ===\n")
|
||||
|
||||
# 3. 提取节点
|
||||
nodes = extract_nodes_from_response(response)
|
||||
if not nodes:
|
||||
print("错误: 未能从 workflow 中提取任何 action 节点")
|
||||
print("请使用 --dump-response 查看原始响应结构")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"\n找到 {len(nodes)} 个 action 节点:")
|
||||
print(f" {'节点 UUID':<40} {'设备 ID':<30} {'动作名':<25} {'Schema'}")
|
||||
print(" " + "-" * 110)
|
||||
for node_uuid, resource_name, template_name, _ in nodes:
|
||||
matched = "✓" if (resource_name in registry_index and
|
||||
template_name in registry_index.get(resource_name, {})) else "✗"
|
||||
print(f" {node_uuid:<40} {resource_name:<30} {template_name:<25} {matched}")
|
||||
|
||||
# 4. 生成模板
|
||||
template = generate_template(nodes, registry_index, opts["rounds"])
|
||||
template["workflow_uuid"] = opts["workflow_uuid"]
|
||||
|
||||
output_path = opts["output"]
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
json.dump(template, f, indent=2, ensure_ascii=False)
|
||||
print(f"\n模板已写入: {output_path}")
|
||||
print(f" 轮次数: {opts['rounds']}")
|
||||
print(f" 节点数/轮: {len(nodes)}")
|
||||
print()
|
||||
print("下一步:")
|
||||
print(" 1. 打开模板文件,将 $TODO 占位符替换为实际值")
|
||||
print(" 2. 删除 _schema_info 字段(仅供参考)")
|
||||
print(" 3. 使用 POST /api/v1/lab/notebook 提交")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
500
.cursor/skills/create-device-skill/SKILL.md
Normal file
500
.cursor/skills/create-device-skill/SKILL.md
Normal file
@@ -0,0 +1,500 @@
|
||||
---
|
||||
name: create-device-skill
|
||||
description: Create a skill for any Uni-Lab device by extracting action schemas from the device registry. Use when the user wants to create a new device skill, add device API documentation, or set up action schemas for a device.
|
||||
---
|
||||
|
||||
# 创建设备 Skill 指南
|
||||
|
||||
本 meta-skill 教你如何为任意 Uni-Lab-OS 设备创建完整的 API 操作技能(参考 `unilab-device-api` 的成功案例)。
|
||||
|
||||
## 数据源
|
||||
|
||||
- **设备注册表**: `unilabos_data/req_device_registry_upload.json`
|
||||
- **结构**: `{ "resources": [{ "id": "<device_id>", "class": { "module": "<python_module:ClassName>", "action_value_mappings": { ... } } }] }`
|
||||
- **生成时机**: `unilab` 启动并完成注册表上传后自动生成
|
||||
- **module 字段**: 格式 `unilabos.devices.xxx.yyy:ClassName`,可转为源码路径 `unilabos/devices/xxx/yyy.py`,阅读源码可了解参数含义和设备行为
|
||||
|
||||
## 创建流程
|
||||
|
||||
### Step 0 — 收集必备信息(缺一不可,否则询问后终止)
|
||||
|
||||
开始前**必须**确认以下 4 项信息全部就绪。如果用户未提供任何一项,**立即询问并终止当前流程**,等用户补齐后再继续。
|
||||
|
||||
向用户提问:「请提供你的 unilab 启动参数,我需要以下信息:」
|
||||
|
||||
#### 必备项 ①:ak / sk(认证凭据)
|
||||
|
||||
来源:启动命令的 `--ak` `--sk` 参数,或 config.py 中的 `ak = "..."` `sk = "..."`。
|
||||
|
||||
获取后立即生成 AUTH token:
|
||||
|
||||
```bash
|
||||
python ./scripts/gen_auth.py <ak> <sk>
|
||||
# 或从 config.py 提取
|
||||
python ./scripts/gen_auth.py --config <config.py>
|
||||
```
|
||||
|
||||
认证算法:`base64(ak:sk)` → `Authorization: Lab <token>`
|
||||
|
||||
#### 必备项 ②:--addr(目标环境)
|
||||
|
||||
决定 API 请求发往哪个服务器。从启动命令的 `--addr` 参数获取:
|
||||
|
||||
| `--addr` 值 | BASE URL |
|
||||
| -------------- | ----------------------------------- |
|
||||
| `test` | `https://leap-lab.test.bohrium.com` |
|
||||
| `uat` | `https://leap-lab.uat.bohrium.com` |
|
||||
| `local` | `http://127.0.0.1:48197` |
|
||||
| 不传(默认) | `https://leap-lab.bohrium.com` |
|
||||
| 其他自定义 URL | 直接使用该 URL |
|
||||
|
||||
#### 必备项 ③:req_device_registry_upload.json(设备注册表)
|
||||
|
||||
数据文件由 `unilab` 启动时自动生成,需要定位它:
|
||||
|
||||
**推断 working_dir**(即 `unilabos_data` 所在目录):
|
||||
|
||||
| 条件 | working_dir 取值 |
|
||||
| -------------------- | -------------------------------------------------------- |
|
||||
| 传了 `--working_dir` | `<working_dir>/unilabos_data/`(若子目录已存在则直接用) |
|
||||
| 仅传了 `--config` | `<config 文件所在目录>/unilabos_data/` |
|
||||
| 都没传 | `<当前工作目录>/unilabos_data/` |
|
||||
|
||||
**按优先级搜索文件**:
|
||||
|
||||
```
|
||||
<推断的 working_dir>/unilabos_data/req_device_registry_upload.json
|
||||
<推断的 working_dir>/req_device_registry_upload.json
|
||||
<workspace 根目录>/unilabos_data/req_device_registry_upload.json
|
||||
```
|
||||
|
||||
也可以直接 Glob 搜索:`**/req_device_registry_upload.json`
|
||||
|
||||
找到后**必须检查文件修改时间**并告知用户:「找到注册表文件 `<路径>`,生成于 `<时间>`。请确认这是最近一次启动生成的。」超过 1 天提醒用户是否需要重新启动 `unilab`。
|
||||
|
||||
**如果文件不存在** → 告知用户先运行 `unilab` 启动命令,等日志出现 `注册表响应数据已保存` 后再执行本流程。**终止。**
|
||||
|
||||
#### 必备项 ④:目标设备
|
||||
|
||||
用户需要明确要为哪个设备创建 skill。可以是设备名称(如「PRCXI 移液站」)或 device_id(如 `liquid_handler.prcxi`)。
|
||||
|
||||
如果用户不确定,运行提取脚本列出所有设备供选择:
|
||||
|
||||
```bash
|
||||
python ./scripts/extract_device_actions.py --registry <找到的文件路径>
|
||||
```
|
||||
|
||||
**四项全部就绪后才进入 Step 1。**
|
||||
|
||||
### Step 1 — 列出可用设备
|
||||
|
||||
运行提取脚本,列出所有设备及 action 数量和 Python 源码路径,让用户选择:
|
||||
|
||||
```bash
|
||||
# 自动搜索(默认在 unilabos_data/ 和当前目录查找)
|
||||
python ./scripts/extract_device_actions.py
|
||||
|
||||
# 指定注册表文件路径
|
||||
python ./scripts/extract_device_actions.py --registry <path/to/req_device_registry_upload.json>
|
||||
```
|
||||
|
||||
脚本输出包含每个设备的 **Python 源码路径**(从 `class.module` 转换),可用于后续阅读源码理解参数含义。
|
||||
|
||||
### Step 2 — 提取 Action Schema
|
||||
|
||||
用户选择设备后,运行提取脚本:
|
||||
|
||||
```bash
|
||||
python ./scripts/extract_device_actions.py [--registry <path>] <device_id> ./skills/<skill-name>/actions/
|
||||
```
|
||||
|
||||
脚本会显示设备的 Python 源码路径和类名,方便阅读源码了解参数含义。
|
||||
|
||||
每个 action 生成一个 JSON 文件,包含:
|
||||
|
||||
- `type` — 作为 API 调用的 `action_type`
|
||||
- `schema` — 完整 JSON Schema(含 `properties.goal.properties` 参数定义)
|
||||
- `goal` — goal 字段映射(含占位符 `$placeholder`)
|
||||
- `goal_default` — 默认值
|
||||
|
||||
### Step 3 — 写 action-index.md
|
||||
|
||||
按模板为每个 action 写条目(**必须包含 `action_type`**):
|
||||
|
||||
```markdown
|
||||
### `<action_name>`
|
||||
|
||||
<用途描述(一句话)>
|
||||
|
||||
- **action_type**: `<从 actions/<name>.json 的 type 字段获取>`
|
||||
- **Schema**: [`actions/<filename>.json`](actions/<filename>.json)
|
||||
- **核心参数**: `param1`, `param2`(从 schema.required 获取)
|
||||
- **可选参数**: `param3`, `param4`
|
||||
- **占位符字段**: `field`(需填入物料信息,值以 `$` 开头)
|
||||
```
|
||||
|
||||
描述规则:
|
||||
|
||||
- **每个 action 必须标注 `action_type`**(从 JSON 的 `type` 字段读取),这是 API #9 调用时的必填参数,传错会导致任务永远卡住
|
||||
- 从 `schema.properties` 读参数列表(schema 已提升为 goal 内容)
|
||||
- 从 `schema.required` 区分核心/可选参数
|
||||
- 按功能分类(移液、枪头、外设等)
|
||||
- 标注 `placeholder_keys` 中的字段类型:
|
||||
- `unilabos_resources` → **ResourceSlot**,填入 `{id, name, uuid}`(id 是路径格式,从资源树取物料节点)
|
||||
- `unilabos_devices` → **DeviceSlot**,填入路径字符串如 `"/host_node"`(从资源树筛选 type=device)
|
||||
- `unilabos_nodes` → **NodeSlot**,填入路径字符串如 `"/PRCXI/PRCXI_Deck"`(资源树中任意节点)
|
||||
- `unilabos_class` → **ClassSlot**,填入类名字符串如 `"container"`(从注册表查找)
|
||||
- `unilabos_formulation` → **FormulationSlot**,填入配方数组 `[{well_name, liquids: [{name, volume}]}]`(well_name 为目标物料的 name)
|
||||
- array 类型字段 → `[{id, name, uuid}, ...]`
|
||||
- 特殊:`create_resource` 的 `res_id`(ResourceSlot)可填不存在的路径
|
||||
|
||||
### Step 4 — 写 SKILL.md
|
||||
|
||||
直接复用 `unilab-device-api` 的 API 模板,修改:
|
||||
|
||||
- 设备名称
|
||||
- Action 数量
|
||||
- 目录列表
|
||||
- Session state 中的 `device_name`
|
||||
- **AUTH 头** — 使用 Step 0 中 `gen_auth.py` 生成的 `Authorization: Lab <token>`(不要硬编码 `Api` 类型的 key)
|
||||
- **Python 源码路径** — 在 SKILL.md 开头注明设备对应的源码文件,方便参考参数含义
|
||||
- **Slot 字段表** — 列出本设备哪些 action 的哪些字段需要填入 Slot(物料/设备/节点/类名)
|
||||
- **action_type 速查表** — 在 API #9 说明后面紧跟一个表格,列出每个 action 对应的 `action_type` 值(从 JSON `type` 字段提取),方便 agent 快速查找而无需打开 JSON 文件
|
||||
|
||||
API 模板结构:
|
||||
|
||||
```markdown
|
||||
## 设备信息
|
||||
|
||||
- device_id, Python 源码路径, 设备类名
|
||||
|
||||
## 前置条件(缺一不可)
|
||||
|
||||
- ak/sk → AUTH, --addr → BASE URL
|
||||
|
||||
## 请求约定
|
||||
|
||||
- Windows 平台必须用 curl.exe(非 PowerShell 的 curl 别名)
|
||||
|
||||
## Session State
|
||||
|
||||
- lab_uuid(通过 GET /edge/lab/info 直接获取,不要问用户), device_name
|
||||
|
||||
## API Endpoints
|
||||
|
||||
# - #1 GET /edge/lab/info → 直接拿到 lab_uuid
|
||||
|
||||
# - #2 创建工作流 POST /lab/workflow/owner → 拼 URL 告知用户
|
||||
|
||||
# - #3 创建节点 POST /edge/workflow/node
|
||||
|
||||
# body: {workflow_uuid, resource_template_name: "<device_id>", node_template_name: "<action_name>"}
|
||||
|
||||
# - #4 删除节点 DELETE /lab/workflow/nodes
|
||||
|
||||
# - #5 更新节点参数 PATCH /lab/workflow/node
|
||||
|
||||
# - #6 查询节点 handles POST /lab/workflow/node-handles
|
||||
|
||||
# body: {node_uuids: ["uuid1","uuid2"]} → 返回各节点的 handle_uuid
|
||||
|
||||
# - #7 批量创建边 POST /lab/workflow/edges
|
||||
|
||||
# body: {edges: [{source_node_uuid, target_node_uuid, source_handle_uuid, target_handle_uuid}]}
|
||||
|
||||
# - #8 启动工作流 POST /lab/workflow/{uuid}/run
|
||||
|
||||
# - #9 运行设备单动作 POST /lab/mcp/run/action(⚠️ action_type 必须从 action-index.md 或 actions/<name>.json 的 type 字段获取,传错会导致任务永远卡住)
|
||||
|
||||
# - #10 查询任务状态 GET /lab/mcp/task/{task_uuid}
|
||||
|
||||
# - #11 运行工作流单节点 POST /lab/mcp/run/workflow/action
|
||||
|
||||
# - #12 获取资源树 GET /lab/material/download/{lab_uuid}
|
||||
|
||||
# - #13 获取工作流模板详情 GET /lab/workflow/template/detail/{workflow_uuid}
|
||||
|
||||
# 返回 workflow 完整结构:data.nodes[] 含每个节点的 uuid、name、param、device_name、handles
|
||||
|
||||
# - #14 按名称查询物料模板 GET /lab/material/template/by-name?lab_uuid=&name=
|
||||
|
||||
# 返回 res_template_uuid,用于 #15 创建物料时的必填字段
|
||||
|
||||
# - #15 创建物料节点 POST /edge/material/node
|
||||
|
||||
# body: {res_template_uuid(从#14获取), name(自定义), display_name, parent_uuid?(从#12获取), ...}
|
||||
|
||||
# - #16 更新物料节点 PUT /edge/material/node
|
||||
|
||||
# body: {uuid(从#12获取), display_name?, description?, init_param_data?, data?, ...}
|
||||
|
||||
## Placeholder Slot 填写规则
|
||||
|
||||
- unilabos_resources → ResourceSlot → {"id":"/path/name","name":"name","uuid":"xxx"}
|
||||
- unilabos_devices → DeviceSlot → "/parent/device" 路径字符串
|
||||
- unilabos_nodes → NodeSlot → "/parent/node" 路径字符串
|
||||
- unilabos_class → ClassSlot → "class_name" 字符串
|
||||
- unilabos_formulation → FormulationSlot → [{well_name, liquids: [{name, volume}]}] 配方数组
|
||||
- 特例:create_resource 的 res_id 允许填不存在的路径
|
||||
- 列出本设备所有 Slot 字段、类型及含义
|
||||
|
||||
## 渐进加载策略
|
||||
|
||||
## 完整工作流 Checklist
|
||||
```
|
||||
|
||||
### Step 5 — 验证
|
||||
|
||||
检查文件完整性:
|
||||
|
||||
- [ ] `SKILL.md` 包含 API endpoint(#1 获取 lab_uuid、#2-#7 工作流/节点/边、#8-#11 运行/查询、#12 资源树、#13 工作流模板详情、#14-#16 物料管理)
|
||||
- [ ] `SKILL.md` 包含 Placeholder Slot 填写规则(ResourceSlot / DeviceSlot / NodeSlot / ClassSlot / FormulationSlot + create_resource 特例)和本设备的 Slot 字段表
|
||||
- [ ] `action-index.md` 列出所有 action 并有描述
|
||||
- [ ] `actions/` 目录中每个 action 有对应 JSON 文件
|
||||
- [ ] JSON 文件包含 `type`, `schema`(已提升为 goal 内容), `goal`, `goal_default`, `placeholder_keys` 字段
|
||||
- [ ] 描述能让 agent 判断该用哪个 action
|
||||
|
||||
## Action JSON 文件结构
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "LiquidHandlerTransfer", // → API 的 action_type
|
||||
"goal": { // goal 字段映射
|
||||
"sources": "sources",
|
||||
"targets": "targets",
|
||||
"tip_racks": "tip_racks",
|
||||
"asp_vols": "asp_vols"
|
||||
},
|
||||
"schema": { // ← 直接是 goal 的 schema(已提升)
|
||||
"type": "object",
|
||||
"properties": { // 参数定义(即请求中 goal 的字段)
|
||||
"sources": { "type": "array", "items": { "type": "object" } },
|
||||
"targets": { "type": "array", "items": { "type": "object" } },
|
||||
"asp_vols": { "type": "array", "items": { "type": "number" } }
|
||||
},
|
||||
"required": [...],
|
||||
"_unilabos_placeholder_info": { // ← Slot 类型标记
|
||||
"sources": "unilabos_resources",
|
||||
"targets": "unilabos_resources",
|
||||
"tip_racks": "unilabos_resources"
|
||||
}
|
||||
},
|
||||
"goal_default": { ... }, // 默认值
|
||||
"placeholder_keys": { // ← 汇总所有 Slot 字段
|
||||
"sources": "unilabos_resources", // ResourceSlot
|
||||
"targets": "unilabos_resources",
|
||||
"tip_racks": "unilabos_resources",
|
||||
"target_device_id": "unilabos_devices" // DeviceSlot
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **注意**:`schema` 已由脚本从原始 `schema.properties.goal` 提升为顶层,直接包含参数定义。
|
||||
> `schema.properties` 中的字段即为 API 创建节点返回的 `data.param` 中的字段,PATCH 更新时直接修改 `param` 即可。
|
||||
|
||||
## Placeholder Slot 类型体系
|
||||
|
||||
`placeholder_keys` / `_unilabos_placeholder_info` 中有 5 种值,对应不同的填写方式:
|
||||
|
||||
| placeholder 值 | Slot 类型 | 填写格式 | 选取范围 |
|
||||
| ---------------------- | --------------- | ----------------------------------------------------- | ----------------------------------------- |
|
||||
| `unilabos_resources` | ResourceSlot | `{"id": "/path/name", "name": "name", "uuid": "xxx"}` | 仅**物料**节点(不含设备) |
|
||||
| `unilabos_devices` | DeviceSlot | `"/parent/device_name"` | 仅**设备**节点(type=device),路径字符串 |
|
||||
| `unilabos_nodes` | NodeSlot | `"/parent/node_name"` | **设备 + 物料**,即所有节点,路径字符串 |
|
||||
| `unilabos_class` | ClassSlot | `"class_name"` | 注册表中已上报的资源类 name |
|
||||
| `unilabos_formulation` | FormulationSlot | `[{well_name, liquids: [{name, volume}]}]` | 资源树中物料节点的 **name**,配合液体配方 |
|
||||
|
||||
### ResourceSlot(`unilabos_resources`)
|
||||
|
||||
最常见的类型。从资源树中选取**物料**节点(孔板、枪头盒、试剂槽等):
|
||||
|
||||
- 单个:`{"id": "/workstation/container1", "name": "container1", "uuid": "ff149a9a-..."}`
|
||||
- 数组:`[{"id": "/path/a", "name": "a", "uuid": "xxx"}, ...]`
|
||||
- `id` 从 parent 计算的路径格式,根据 action 语义选择正确的物料
|
||||
|
||||
> **特例**:`create_resource` 的 `res_id`,目标物料可能尚不存在,直接填期望路径,不需要 uuid。
|
||||
|
||||
### DeviceSlot / NodeSlot / ClassSlot
|
||||
|
||||
- **DeviceSlot**(`unilabos_devices`):路径字符串如 `"/host_node"`,仅 type=device 的节点
|
||||
- **NodeSlot**(`unilabos_nodes`):路径字符串如 `"/PRCXI/PRCXI_Deck"`,设备 + 物料均可选
|
||||
- **ClassSlot**(`unilabos_class`):类名字符串如 `"container"`,从 `req_resource_registry_upload.json` 查找
|
||||
|
||||
### FormulationSlot(`unilabos_formulation`)
|
||||
|
||||
描述**液体配方**:向哪些容器中加入哪些液体及体积。
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"sample_uuid": "",
|
||||
"well_name": "bottle_A1",
|
||||
"liquids": [{ "name": "LiPF6", "volume": 0.6 }]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
- `well_name` — 目标物料的 **name**(从资源树取,不是 `id` 路径)
|
||||
- `liquids[]` — 液体列表,每条含 `name`(试剂名)和 `volume`(体积,单位由上下文决定;pylabrobot 内部统一 uL)
|
||||
- `sample_uuid` — 样品 UUID,无样品传 `""`
|
||||
- 与 ResourceSlot 的区别:ResourceSlot 指向物料本身,FormulationSlot 引用物料名并附带配方信息
|
||||
|
||||
### 通过 API #12 获取资源树
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/material/download/$lab_uuid" -H "$AUTH"
|
||||
```
|
||||
|
||||
注意 `lab_uuid` 在路径中(不是查询参数)。返回结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"nodes": [
|
||||
{"name": "host_node", "uuid": "c3ec1e68-...", "type": "device", "parent": ""},
|
||||
{"name": "PRCXI", "uuid": "e249c9a6-...", "type": "device", "parent": ""},
|
||||
{"name": "PRCXI_Deck", "uuid": "fb6a8b71-...", "type": "deck", "parent": "PRCXI"}
|
||||
],
|
||||
"edges": [...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `data.nodes[]` — 所有节点(设备 + 物料),每个节点含 `name`、`uuid`、`type`、`parent`
|
||||
- `type` 区分设备(`device`)和物料(`deck`、`container`、`resource` 等)
|
||||
- `parent` 为父节点名称(空字符串表示顶级)
|
||||
- 填写 Slot 时根据 placeholder 类型筛选:ResourceSlot 取非 device 节点,DeviceSlot 取 device 节点
|
||||
- 创建/更新物料时:`parent_uuid` 取父节点的 `uuid`,更新目标的 `uuid` 取节点自身的 `uuid`
|
||||
|
||||
## 物料管理 API
|
||||
|
||||
设备 Skill 除了设备动作外,还需支持物料节点的创建和参数设定,用于在资源树中动态管理物料。
|
||||
|
||||
典型流程:先通过 **#14 按名称查询模板** 获取 `res_template_uuid` → 再通过 **#15 创建物料** → 之后可通过 **#16 更新物料** 修改属性。更新时需要的 `uuid` 和 `parent_uuid` 均从 **#12 资源树下载** 获取。
|
||||
|
||||
### API #14 — 按名称查询物料模板
|
||||
|
||||
创建物料前,需要先获取物料模板的 UUID。通过模板名称查询:
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/material/template/by-name?lab_uuid=$lab_uuid&name=<template_name>" -H "$AUTH"
|
||||
```
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
| ---------- | ------ | -------------------------------- |
|
||||
| `lab_uuid` | **是** | 实验室 UUID(从 API #1 获取) |
|
||||
| `name` | **是** | 物料模板名称(如 `"container"`) |
|
||||
|
||||
返回 `code: 0` 时,**`data.uuid`** 即为 `res_template_uuid`,用于 API #15 创建物料。返回还包含 `name`、`resource_type`、`handles`、`config_infos` 等模板元信息。
|
||||
|
||||
模板不存在时返回 `code: 10002`,`data` 为空对象。模板名称来自资源注册表中已注册的资源类型。
|
||||
|
||||
### API #15 — 创建物料节点
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/edge/material/node" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '<request_body>'
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"res_template_uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
"name": "my_custom_bottle",
|
||||
"display_name": "自定义瓶子",
|
||||
"parent_uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
"type": "",
|
||||
"init_param_data": {},
|
||||
"schema": {},
|
||||
"data": {
|
||||
"liquids": [["water", 1000, "uL"]],
|
||||
"max_volume": 50000
|
||||
},
|
||||
"plate_well_datas": {},
|
||||
"plate_reagent_datas": {},
|
||||
"pose": {},
|
||||
"model": {}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 必填 | 类型 | 数据来源 | 说明 |
|
||||
| --------------------- | ------ | ------------- | ----------------------------------- | -------------------------------------- |
|
||||
| `res_template_uuid` | **是** | string (UUID) | **API #14** 按名称查询获取 | 物料模板 UUID |
|
||||
| `name` | 否 | string | **用户自定义** | 节点名称(标识符),可自由命名 |
|
||||
| `display_name` | 否 | string | 用户自定义 | 显示名称(UI 展示用) |
|
||||
| `parent_uuid` | 否 | string (UUID) | **API #12** 资源树中父节点的 `uuid` | 父节点,为空则创建顶级节点 |
|
||||
| `type` | 否 | string | 从模板继承 | 节点类型 |
|
||||
| `init_param_data` | 否 | object | 用户指定 | 初始化参数,覆盖模板默认值 |
|
||||
| `data` | 否 | object | 用户指定 | 节点数据,container 见下方 data 格式 |
|
||||
| `plate_well_datas` | 否 | object | 用户指定 | 孔板子节点数据(创建带孔位的板时使用) |
|
||||
| `plate_reagent_datas` | 否 | object | 用户指定 | 试剂关联数据 |
|
||||
| `schema` | 否 | object | 从模板继承 | 自定义 schema,不传则从模板继承 |
|
||||
| `pose` | 否 | object | 用户指定 | 位姿信息 |
|
||||
| `model` | 否 | object | 用户指定 | 3D 模型信息 |
|
||||
|
||||
#### container 的 `data` 格式
|
||||
|
||||
> **体积单位统一为 uL(微升)**。pylabrobot 体系中所有体积值(`max_volume`、`liquids` 中的 volume)均为 uL。外部如果是 mL 需乘 1000 转换。
|
||||
|
||||
```json
|
||||
{
|
||||
"liquids": [["water", 1000, "uL"], ["ethanol", 500, "uL"]],
|
||||
"max_volume": 50000
|
||||
}
|
||||
```
|
||||
|
||||
- `liquids` — 液体列表,每条为 `[液体名称, 体积(uL), 单位字符串]`
|
||||
- `max_volume` — 容器最大容量(uL),如 50 mL = 50000 uL
|
||||
|
||||
### API #16 — 更新物料节点
|
||||
|
||||
```bash
|
||||
curl -s -X PUT "$BASE/api/v1/edge/material/node" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '<request_body>'
|
||||
```
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
"parent_uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
"display_name": "新显示名称",
|
||||
"description": "新描述",
|
||||
"init_param_data": {},
|
||||
"data": {},
|
||||
"pose": {},
|
||||
"schema": {},
|
||||
"extra": {}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 必填 | 类型 | 数据来源 | 说明 |
|
||||
| ----------------- | ------ | ------------- | ------------------------------------- | ---------------- |
|
||||
| `uuid` | **是** | string (UUID) | **API #12** 资源树中目标节点的 `uuid` | 要更新的物料节点 |
|
||||
| `parent_uuid` | 否 | string (UUID) | API #12 资源树 | 移动到新父节点 |
|
||||
| `display_name` | 否 | string | 用户指定 | 更新显示名称 |
|
||||
| `description` | 否 | string | 用户指定 | 更新描述 |
|
||||
| `init_param_data` | 否 | object | 用户指定 | 更新初始化参数 |
|
||||
| `data` | 否 | object | 用户指定 | 更新节点数据 |
|
||||
| `pose` | 否 | object | 用户指定 | 更新位姿 |
|
||||
| `schema` | 否 | object | 用户指定 | 更新 schema |
|
||||
| `extra` | 否 | object | 用户指定 | 更新扩展数据 |
|
||||
|
||||
> 只传需要更新的字段,未传的字段保持不变。
|
||||
|
||||
## 最终目录结构
|
||||
|
||||
```
|
||||
./<skill-name>/
|
||||
├── SKILL.md # API 端点 + 渐进加载指引
|
||||
├── action-index.md # 动作索引:描述/用途/核心参数
|
||||
└── actions/ # 每个 action 的完整 JSON Schema
|
||||
├── action1.json
|
||||
├── action2.json
|
||||
└── ...
|
||||
```
|
||||
@@ -0,0 +1,200 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
从 req_device_registry_upload.json 中提取指定设备的 action schema。
|
||||
|
||||
用法:
|
||||
# 列出所有设备及 action 数量(自动搜索注册表文件)
|
||||
python extract_device_actions.py
|
||||
|
||||
# 指定注册表文件路径
|
||||
python extract_device_actions.py --registry <path/to/req_device_registry_upload.json>
|
||||
|
||||
# 提取指定设备的 action 到目录
|
||||
python extract_device_actions.py <device_id> <output_dir>
|
||||
python extract_device_actions.py --registry <path> <device_id> <output_dir>
|
||||
|
||||
示例:
|
||||
python extract_device_actions.py --registry unilabos_data/req_device_registry_upload.json
|
||||
python extract_device_actions.py liquid_handler.prcxi .cursor/skills/unilab-device-api/actions/
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
REGISTRY_FILENAME = "req_device_registry_upload.json"
|
||||
|
||||
def find_registry(explicit_path=None):
|
||||
"""
|
||||
查找 req_device_registry_upload.json 文件。
|
||||
|
||||
搜索优先级:
|
||||
1. 用户通过 --registry 显式指定的路径
|
||||
2. <cwd>/unilabos_data/req_device_registry_upload.json
|
||||
3. <cwd>/req_device_registry_upload.json
|
||||
4. <script所在目录>/../../.. (workspace根) 下的 unilabos_data/
|
||||
5. 向上逐级搜索父目录(最多 5 层)
|
||||
"""
|
||||
if explicit_path:
|
||||
if os.path.isfile(explicit_path):
|
||||
return explicit_path
|
||||
if os.path.isdir(explicit_path):
|
||||
fp = os.path.join(explicit_path, REGISTRY_FILENAME)
|
||||
if os.path.isfile(fp):
|
||||
return fp
|
||||
print(f"警告: 指定的路径不存在: {explicit_path}")
|
||||
return None
|
||||
|
||||
candidates = [
|
||||
os.path.join("unilabos_data", REGISTRY_FILENAME),
|
||||
REGISTRY_FILENAME,
|
||||
]
|
||||
|
||||
for c in candidates:
|
||||
if os.path.isfile(c):
|
||||
return c
|
||||
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
workspace_root = os.path.normpath(os.path.join(script_dir, "..", "..", ".."))
|
||||
for c in candidates:
|
||||
path = os.path.join(workspace_root, c)
|
||||
if os.path.isfile(path):
|
||||
return path
|
||||
|
||||
cwd = os.getcwd()
|
||||
for _ in range(5):
|
||||
parent = os.path.dirname(cwd)
|
||||
if parent == cwd:
|
||||
break
|
||||
cwd = parent
|
||||
for c in candidates:
|
||||
path = os.path.join(cwd, c)
|
||||
if os.path.isfile(path):
|
||||
return path
|
||||
|
||||
return None
|
||||
|
||||
def load_registry(path):
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
def list_devices(data):
|
||||
"""列出所有包含 action_value_mappings 的设备,同时返回 module 路径"""
|
||||
resources = data.get('resources', [])
|
||||
devices = []
|
||||
for res in resources:
|
||||
rid = res.get('id', '')
|
||||
cls = res.get('class', {})
|
||||
avm = cls.get('action_value_mappings', {})
|
||||
module = cls.get('module', '')
|
||||
if avm:
|
||||
devices.append((rid, len(avm), module))
|
||||
return devices
|
||||
|
||||
def flatten_schema_to_goal(action_data):
|
||||
"""将 schema 中嵌套的 goal 内容提升为顶层 schema,去掉 feedback/result 包装"""
|
||||
schema = action_data.get('schema', {})
|
||||
goal_schema = schema.get('properties', {}).get('goal', {})
|
||||
if goal_schema:
|
||||
action_data = dict(action_data)
|
||||
action_data['schema'] = goal_schema
|
||||
return action_data
|
||||
|
||||
|
||||
def extract_actions(data, device_id, output_dir):
|
||||
"""提取指定设备的 action schema 到独立 JSON 文件"""
|
||||
resources = data.get('resources', [])
|
||||
for res in resources:
|
||||
if res.get('id') == device_id:
|
||||
cls = res.get('class', {})
|
||||
module = cls.get('module', '')
|
||||
avm = cls.get('action_value_mappings', {})
|
||||
if not avm:
|
||||
print(f"设备 {device_id} 没有 action_value_mappings")
|
||||
return []
|
||||
|
||||
if module:
|
||||
py_path = module.split(":")[0].replace(".", "/") + ".py"
|
||||
class_name = module.split(":")[-1] if ":" in module else ""
|
||||
print(f"Python 源码: {py_path}")
|
||||
if class_name:
|
||||
print(f"设备类: {class_name}")
|
||||
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
written = []
|
||||
for action_name in sorted(avm.keys()):
|
||||
action_data = flatten_schema_to_goal(avm[action_name])
|
||||
filename = action_name.replace('-', '_') + '.json'
|
||||
filepath = os.path.join(output_dir, filename)
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
json.dump(action_data, f, indent=2, ensure_ascii=False)
|
||||
written.append(filename)
|
||||
print(f" {filepath}")
|
||||
return written
|
||||
|
||||
print(f"设备 {device_id} 未找到")
|
||||
return []
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
explicit_registry = None
|
||||
|
||||
if "--registry" in args:
|
||||
idx = args.index("--registry")
|
||||
if idx + 1 < len(args):
|
||||
explicit_registry = args[idx + 1]
|
||||
args = args[:idx] + args[idx + 2:]
|
||||
else:
|
||||
print("错误: --registry 需要指定路径")
|
||||
sys.exit(1)
|
||||
|
||||
registry_path = find_registry(explicit_registry)
|
||||
if not registry_path:
|
||||
print(f"错误: 找不到 {REGISTRY_FILENAME}")
|
||||
print()
|
||||
print("解决方法:")
|
||||
print(" 1. 先运行 unilab 启动命令,等待注册表生成")
|
||||
print(" 2. 用 --registry 指定文件路径:")
|
||||
print(f" python {sys.argv[0]} --registry <path/to/{REGISTRY_FILENAME}>")
|
||||
print()
|
||||
print("搜索过的路径:")
|
||||
for p in [
|
||||
os.path.join("unilabos_data", REGISTRY_FILENAME),
|
||||
REGISTRY_FILENAME,
|
||||
os.path.join("<workspace_root>", "unilabos_data", REGISTRY_FILENAME),
|
||||
]:
|
||||
print(f" - {p}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"注册表: {registry_path}")
|
||||
mtime = os.path.getmtime(registry_path)
|
||||
gen_time = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S")
|
||||
size_mb = os.path.getsize(registry_path) / (1024 * 1024)
|
||||
print(f"生成时间: {gen_time} (文件大小: {size_mb:.1f} MB)")
|
||||
data = load_registry(registry_path)
|
||||
|
||||
if len(args) == 0:
|
||||
devices = list_devices(data)
|
||||
print(f"\n找到 {len(devices)} 个设备:")
|
||||
print(f"{'设备 ID':<50} {'Actions':>7} {'Python 模块'}")
|
||||
print("-" * 120)
|
||||
for did, count, module in sorted(devices, key=lambda x: x[0]):
|
||||
py_path = module.split(":")[0].replace(".", "/") + ".py" if module else ""
|
||||
print(f"{did:<50} {count:>7} {py_path}")
|
||||
|
||||
elif len(args) == 2:
|
||||
device_id = args[0]
|
||||
output_dir = args[1]
|
||||
print(f"\n提取 {device_id} 的 actions 到 {output_dir}/")
|
||||
written = extract_actions(data, device_id, output_dir)
|
||||
if written:
|
||||
print(f"\n共写入 {len(written)} 个 action 文件")
|
||||
|
||||
else:
|
||||
print("用法:")
|
||||
print(" python extract_device_actions.py [--registry <path>] # 列出设备")
|
||||
print(" python extract_device_actions.py [--registry <path>] <device_id> <dir> # 提取 actions")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
69
.cursor/skills/create-device-skill/scripts/gen_auth.py
Normal file
69
.cursor/skills/create-device-skill/scripts/gen_auth.py
Normal file
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
从 ak/sk 生成 UniLab API Authorization header。
|
||||
|
||||
算法: base64(ak:sk) → "Authorization: Lab <token>"
|
||||
|
||||
用法:
|
||||
python gen_auth.py <ak> <sk>
|
||||
python gen_auth.py --config <config.py>
|
||||
|
||||
示例:
|
||||
python gen_auth.py myak mysk
|
||||
python gen_auth.py --config experiments/config.py
|
||||
"""
|
||||
import base64
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
def gen_auth(ak: str, sk: str) -> str:
|
||||
token = base64.b64encode(f"{ak}:{sk}".encode("utf-8")).decode("utf-8")
|
||||
return token
|
||||
|
||||
|
||||
def extract_from_config(config_path: str) -> tuple:
|
||||
"""从 config.py 中提取 ak 和 sk"""
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
ak_match = re.search(r'''ak\s*=\s*["']([^"']+)["']''', content)
|
||||
sk_match = re.search(r'''sk\s*=\s*["']([^"']+)["']''', content)
|
||||
if not ak_match or not sk_match:
|
||||
return None, None
|
||||
return ak_match.group(1), sk_match.group(1)
|
||||
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
|
||||
if len(args) == 2 and args[0] == "--config":
|
||||
ak, sk = extract_from_config(args[1])
|
||||
if not ak or not sk:
|
||||
print(f"错误: 在 {args[1]} 中未找到 ak/sk 配置")
|
||||
print("期望格式: ak = \"xxx\" sk = \"xxx\"")
|
||||
sys.exit(1)
|
||||
print(f"配置文件: {args[1]}")
|
||||
elif len(args) == 2:
|
||||
ak, sk = args
|
||||
else:
|
||||
print("用法:")
|
||||
print(" python gen_auth.py <ak> <sk>")
|
||||
print(" python gen_auth.py --config <config.py>")
|
||||
sys.exit(1)
|
||||
|
||||
token = gen_auth(ak, sk)
|
||||
print(f"ak: {ak}")
|
||||
print(f"sk: {sk}")
|
||||
print()
|
||||
print(f"Authorization header:")
|
||||
print(f" Authorization: Lab {token}")
|
||||
print()
|
||||
print(f"curl 用法:")
|
||||
print(f' curl -H "Authorization: Lab {token}" ...')
|
||||
print()
|
||||
print(f"Shell 变量:")
|
||||
print(f' AUTH="Authorization: Lab {token}"')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
450
.cursor/skills/filter-workflow-by-tags/SKILL.md
Normal file
450
.cursor/skills/filter-workflow-by-tags/SKILL.md
Normal file
@@ -0,0 +1,450 @@
|
||||
---
|
||||
name: filter-workflow-by-tags
|
||||
description: Query backend workflow list, aggregate all tags, and filter workflows by domain/scenario requirements using tags. Use when the user wants to search workflows, find workflows by tags, list available workflow tags, filter workflows by category/domain/scenario, or mentions 工作流筛选/标签查询/workflow tags/按领域查找工作流.
|
||||
---
|
||||
# Uni-Lab 工作流标签筛选指南
|
||||
|
||||
通过 Uni-Lab 云端 API 查询工作流列表,汇总所有可用标签(tags),并根据领域和场景要求筛选工作流。
|
||||
|
||||
> **重要**:本指南中的 `Authorization: Lab <token>` 是 **Uni-Lab 平台专用的认证方式**,`Lab` 是 Uni-Lab 的 auth scheme 关键字,**不是** HTTP Basic 认证。请勿将其替换为 `Basic`。
|
||||
|
||||
## 使用模式识别
|
||||
|
||||
**用户可能一开始就给出场景目标**(如"我要做有机合成实验"、"找柱层析相关的 protocol")。此时:
|
||||
|
||||
1. **识别场景关键词** → 映射到可能的 tags(如 synthesis、organic、chromatography、purification)
|
||||
2. **直接执行完整流程**(获取 ak/sk/addr → 拉取所有工作流 → 汇总 tags → 按场景筛选)
|
||||
3. **展示筛选结果** → 引导用户从候选 workflow 中**选择明确的实验 protocol**
|
||||
4. **如果用户确认某个 workflow** → 记录 `workflow_uuid`,准备对接“与其他 Skill 的协作”
|
||||
|
||||
**如果用户未给场景目标**,则按标准 checklist 询问筛选条件。
|
||||
|
||||
---
|
||||
|
||||
## 前置条件
|
||||
|
||||
使用本指南前,**必须**先确认以下信息。如果缺少任何一项,**立即向用户询问并终止**,等补齐后再继续。
|
||||
|
||||
### 1. ak / sk → AUTH
|
||||
|
||||
询问用户的启动参数,从 `--ak` `--sk` 或 config.py 中获取。
|
||||
|
||||
生成 AUTH token:
|
||||
|
||||
```bash
|
||||
python -c "import base64,sys; print('Authorization: Lab ' + base64.b64encode(f'{sys.argv[1]}:{sys.argv[2]}'.encode()).decode())" <ak> <sk>
|
||||
```
|
||||
|
||||
### 2. --addr → BASE URL
|
||||
|
||||
| `--addr` 值 | BASE |
|
||||
| ------------- | ------------------------------------- |
|
||||
| `test` | `https://leap-lab.test.bohrium.com` |
|
||||
| `uat` | `https://leap-lab.uat.bohrium.com` |
|
||||
| `local` | `http://127.0.0.1:48197` |
|
||||
| 不传(默认) | `https://leap-lab.bohrium.com` |
|
||||
|
||||
确认后设置:
|
||||
|
||||
```bash
|
||||
BASE="<根据 addr 确定的 URL>"
|
||||
AUTH="Authorization: Lab <上面命令输出的 token>"
|
||||
```
|
||||
|
||||
### 3. lab_uuid(实验室 UUID)
|
||||
|
||||
如果用户未提供 `lab_uuid`,通过以下 API 自动获取:
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/edge/lab/info" -H "$AUTH"
|
||||
```
|
||||
|
||||
返回 `data.uuid` 即为 `lab_uuid`。
|
||||
|
||||
**三项全部就绪后才可开始。**
|
||||
|
||||
## Session State
|
||||
|
||||
在整个对话过程中,agent 需要记住以下状态:
|
||||
|
||||
- `lab_uuid` — 实验室 UUID
|
||||
- `all_workflows` — 完整工作流列表(分页获取后缓存到内存或临时文件)
|
||||
- `all_tags` — 所有工作流的标签汇总
|
||||
|
||||
---
|
||||
|
||||
## API 端点
|
||||
|
||||
### 查询工作流列表(支持分页)
|
||||
|
||||
```
|
||||
GET $BASE/api/v1/lab/workflow/owner/list?page=<page>&page_size=<page_size>&lab_uuid=$lab_uuid
|
||||
```
|
||||
|
||||
**参数:**
|
||||
|
||||
- `page` — 页码,从 1 开始
|
||||
- `page_size` — 每页数量,建议 1000
|
||||
- `lab_uuid` — 实验室 UUID
|
||||
|
||||
**返回结构:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"has_more": true,
|
||||
"data": [
|
||||
{
|
||||
"uuid": "9661bba2-1b9f-4687-a63d-910245df174b",
|
||||
"name": "Untitled",
|
||||
"description": "",
|
||||
"user_id": "114211",
|
||||
"published": false,
|
||||
"tags": null
|
||||
},
|
||||
{
|
||||
"uuid": "e0436638-190b-46bc-b1a1-2711d9602f6a",
|
||||
"name": "Synthesis v2",
|
||||
"user_id": "114211",
|
||||
"published": true,
|
||||
"tags": ["synthesis", "organic"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**字段说明:**
|
||||
|
||||
- `has_more` — 若为 `true`,需要继续请求 `page+1`
|
||||
- `tags` — 可能为 `null`、空数组或字符串数组;聚合时必须容忍 `null`
|
||||
|
||||
### 启动工作流(直接运行)
|
||||
|
||||
```
|
||||
POST $BASE/api/v1/lab/workflow/<workflow_uuid>/run
|
||||
```
|
||||
|
||||
**用途:** 直接启动一个 workflow 的默认执行(使用模板中预设的参数),无需创建 notebook。适用于快速测试或无参数变化的重复执行。
|
||||
|
||||
**请求体:** 空 JSON `{}` 或省略
|
||||
|
||||
**返回:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": "<run_uuid>"
|
||||
}
|
||||
```
|
||||
|
||||
- `run_uuid` — 本次执行的唯一标识(不是 notebook UUID)
|
||||
|
||||
**注意:**
|
||||
|
||||
- 该接口会使用 workflow 模板中保存的默认参数直接执行
|
||||
- 如果 workflow 需要动态参数(如 CSV 路径、样品 UUID),应使用 `POST /lab/notebook` 创建 notebook 并传入 `node_params`
|
||||
- 返回的 `run_uuid` 可直接传入下方「查询任务状态」接口查询实时进度
|
||||
|
||||
### 查询任务状态
|
||||
|
||||
```
|
||||
GET $BASE/api/v1/lab/mcp/task/<task_uuid>
|
||||
```
|
||||
|
||||
**用途:** 查询由 `POST /lab/workflow/<uuid>/run` 返回的 `run_uuid`(即 task_uuid)的实时执行状态,包括整体状态和每个节点(JOS:Job On Station)的执行详情。
|
||||
|
||||
**路径参数:**
|
||||
|
||||
- `task_uuid` — 等同于启动工作流接口返回的 `run_uuid`
|
||||
|
||||
**返回:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"status": "running",
|
||||
"jos_status": [
|
||||
{
|
||||
"uuid": "d0e24bfe-8d99-450e-b19d-f25849dfbaad",
|
||||
"node_name": "PRCXI_BioER_96_wellplate_slot_1",
|
||||
"action_name": "create_resource",
|
||||
"status": "success",
|
||||
"return_info": {
|
||||
"suc": true,
|
||||
"error": "",
|
||||
"return_value": { ... }
|
||||
}
|
||||
},
|
||||
{
|
||||
"uuid": "...",
|
||||
"node_name": "...",
|
||||
"action_name": "transfer_liquid",
|
||||
"status": "pending",
|
||||
"return_info": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**字段说明:**
|
||||
|
||||
- `data.status` — 整体任务状态
|
||||
- `running` — 正在执行(至少一个节点 pending 或 running)
|
||||
- `success` — 全部节点成功
|
||||
- `failed` — 有节点失败
|
||||
- `data.jos_status[]` — 节点级执行列表(按执行顺序)
|
||||
- `uuid` — 节点执行实例 UUID
|
||||
- `node_name` — 节点名称(资源/设备名或工位名)
|
||||
- `action_name` — 动作类型(`create_resource`、`transfer_liquid`、`centrifuge`、等)
|
||||
- `status` — 节点状态:`success`、`pending`、`running`、`failed`
|
||||
- `return_info` — 执行返回,失败时 `suc=false` 且 `error` 有错误信息
|
||||
|
||||
**注意:**
|
||||
|
||||
- 此接口的 `task_uuid` **是** `POST /lab/workflow/<uuid>/run` 返回的 `run_uuid`,二者为同一个 ID 的不同称呼
|
||||
- **不要**把 notebook UUID(`POST /lab/notebook` 返回)传进来——那条路径用 `GET /lab/notebook/status` 查询
|
||||
- `jos_status` 数组按节点执行顺序给出;从 pending 数量可以估算剩余进度
|
||||
- 返回体可能较大(`return_info.return_value` 中可能包含完整 resource tree),可在脚本中只提取 `status` + `node_name` + `action_name` 做摘要
|
||||
|
||||
**状态轮询示例:**
|
||||
|
||||
```bash
|
||||
# 每 5 秒轮询一次直至完成
|
||||
TASK="b183d97e-d2b5-4b24-b14b-820df04d87c0"
|
||||
while :; do
|
||||
st=$(curl -s -X GET "$BASE/api/v1/lab/mcp/task/$TASK" -H "$AUTH" \
|
||||
| python3 -c "import json,sys; d=json.load(sys.stdin)['data']; \
|
||||
print(d['status'], '|', sum(1 for j in d['jos_status'] if j['status']=='success'), '/', len(d['jos_status']))")
|
||||
echo "$(date +%H:%M:%S) $st"
|
||||
[[ "$st" == success* || "$st" == failed* ]] && break
|
||||
sleep 5
|
||||
done
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 完整工作流 Checklist
|
||||
|
||||
```
|
||||
Task Progress:
|
||||
- [ ] Step 0: 识别用户是否已给出场景目标(如"有机合成"、"柱层析")
|
||||
- 若已给出 → 记录场景关键词,自动进入后续步骤
|
||||
- 若未给出 → 在 Step 6 询问用户
|
||||
- [ ] Step 1: 确认 ak/sk → 生成 AUTH token
|
||||
- [ ] Step 2: 确认 --addr → 设置 BASE URL
|
||||
- [ ] Step 3: GET /edge/lab/info → 获取 lab_uuid(如用户未提供)
|
||||
- [ ] Step 4: 分页获取所有工作流(从 page=1 开始直到 has_more=false)
|
||||
- [ ] Step 5: 汇总所有非空 tags → 生成 all_tags(去重、排序、附出现次数)
|
||||
- [ ] Step 6: 根据场景关键词(Step 0 或新询问)在 all_tags 中做语义映射 → 确定候选 tags
|
||||
- 若语义映射不唯一,列出候选 tags 让用户确认
|
||||
- [ ] Step 7: 按候选 tags 筛选工作流(默认 any 模式,召回优先)
|
||||
- [ ] Step 8: 展示筛选结果(uuid、name、description、tags、published)
|
||||
- [ ] Step 9: 引导用户从结果中选择**明确的实验 protocol**
|
||||
- 若结果只有 1 条 → 直接确认该 workflow_uuid
|
||||
- 若结果 2–10 条 → 让用户按编号选择
|
||||
- 若结果过多 → 提示收紧条件(加 tag、切换 all 模式、仅 published)
|
||||
- 若结果为空 → 放宽条件(去掉最稀有 tag)或提示用户换关键词
|
||||
- [ ] Step 10: 记录用户选中的 workflow_uuid,并提示提交实验或查看详情
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 推荐路径:使用脚本
|
||||
|
||||
同目录下提供 `scripts/filter_workflows.py`,一次完成分页抓取、标签聚合与筛选:
|
||||
|
||||
```bash
|
||||
# 1. 仅汇总标签(不筛选)
|
||||
python scripts/filter_workflows.py \
|
||||
--auth "<Lab base64token>" \
|
||||
--base "$BASE" \
|
||||
--lab-uuid "$lab_uuid" \
|
||||
--summary-only
|
||||
|
||||
# 2. 按标签筛选(ANY 模式:包含任一)
|
||||
python scripts/filter_workflows.py \
|
||||
--auth "<Lab base64token>" \
|
||||
--base "$BASE" \
|
||||
--lab-uuid "$lab_uuid" \
|
||||
--tags synthesis organic \
|
||||
--mode any
|
||||
|
||||
# 3. 按标签筛选(ALL 模式:必须同时包含)
|
||||
python scripts/filter_workflows.py \
|
||||
--auth "<Lab base64token>" \
|
||||
--base "$BASE" \
|
||||
--lab-uuid "$lab_uuid" \
|
||||
--tags synthesis organic \
|
||||
--mode all \
|
||||
--output filtered.json
|
||||
|
||||
# 4. 仅筛选已发布
|
||||
python scripts/filter_workflows.py \
|
||||
--auth "<Lab base64token>" \
|
||||
--base "$BASE" \
|
||||
--lab-uuid "$lab_uuid" \
|
||||
--tags synthesis \
|
||||
--published-only
|
||||
```
|
||||
|
||||
**`--auth` 参数说明**:传入 `Authorization` 头中 `Lab` 之后的 base64 token(不带 `Lab ` 前缀),脚本内部会自动补上 scheme。
|
||||
|
||||
**输出结构:**
|
||||
|
||||
```json
|
||||
{
|
||||
"total_workflows": 150,
|
||||
"tag_counts": {"synthesis": 12, "organic": 8, "analysis": 5},
|
||||
"all_tags": ["analysis", "organic", "synthesis"],
|
||||
"filter": {"tags": ["synthesis", "organic"], "mode": "any"},
|
||||
"filtered_workflows": [
|
||||
{"uuid": "...", "name": "...", "description": "...", "tags": [...], "published": true}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 手动路径:curl + jq
|
||||
|
||||
如果脚本不可用或环境缺少 Python,可用 shell 实现。
|
||||
|
||||
### 1. 分页抓取(写入 `all_workflows.json`)
|
||||
|
||||
```bash
|
||||
page=1
|
||||
echo "[]" > all_workflows.json
|
||||
|
||||
while :; do
|
||||
resp=$(curl -s -X GET \
|
||||
"$BASE/api/v1/lab/workflow/owner/list?page=$page&page_size=1000&lab_uuid=$lab_uuid" \
|
||||
-H "$AUTH")
|
||||
|
||||
page_data=$(echo "$resp" | jq -c '.data.data // []')
|
||||
jq -c --argjson p "$page_data" '. + $p' all_workflows.json > _tmp.json && mv _tmp.json all_workflows.json
|
||||
|
||||
has_more=$(echo "$resp" | jq -r '.data.has_more')
|
||||
[ "$has_more" != "true" ] && break
|
||||
page=$((page + 1))
|
||||
done
|
||||
|
||||
echo "Total: $(jq 'length' all_workflows.json)"
|
||||
```
|
||||
|
||||
### 2. 汇总所有标签(含出现次数)
|
||||
|
||||
```bash
|
||||
jq '[.[].tags // [] | .[]] | group_by(.) | map({tag: .[0], count: length}) | sort_by(-.count)' \
|
||||
all_workflows.json
|
||||
```
|
||||
|
||||
### 3. 按标签筛选
|
||||
|
||||
```bash
|
||||
# ANY:包含任一指定标签
|
||||
jq --argjson want '["synthesis","organic"]' \
|
||||
'[.[] | select((.tags // []) | any(. as $t | $want | index($t)))]' \
|
||||
all_workflows.json
|
||||
|
||||
# ALL:同时包含所有指定标签
|
||||
jq --argjson want '["synthesis","organic"]' \
|
||||
'[.[] | select(($want | all(. as $w | (.tags // []) | index($w))))]' \
|
||||
all_workflows.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 筛选策略
|
||||
|
||||
agent 拿到用户的「领域 + 场景」自然语言描述时,按如下顺序选择 tag:
|
||||
|
||||
1. **优先用户显式指定的 tags**:若用户明确给出标签词,直接精确匹配。
|
||||
2. **从 all_tags 中做语义映射**:若用户描述是自然语言(如"有机合成、柱层析"),在 all_tags 中找语义相关项(如 `synthesis`、`organic`、`chromatography`)。必要时展示候选 tag 让用户确认。
|
||||
3. **模式选择**:
|
||||
- 默认 `any`(更多召回),给出 tag 集合的并集匹配
|
||||
- 用户强调"必须同时满足"时用 `all`
|
||||
4. **空结果兜底**:如果筛选为空,放宽条件(去掉最稀有 tag、切换 any 模式),并提醒用户。
|
||||
|
||||
---
|
||||
|
||||
## 引导到明确的 Protocol
|
||||
|
||||
筛选完成后,**最终目标是让用户确认一个具体的 workflow_uuid**,而不是停留在"一堆候选"上。按结果数量采取不同策略:
|
||||
|
||||
| 结果数量 | 策略 |
|
||||
| --------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 0 条 | 放宽筛选(去掉最稀有 tag → 切换 any 模式 → 去掉 `--published-only`)。仍为空则提示换关键词,或列出 `all_tags` 让用户重新选。 |
|
||||
| 1 条 | 直接确认:"找到唯一匹配:`<name>` (uuid `<uuid>`),是否用它?"用户确认后记录 `workflow_uuid`。 |
|
||||
| 2–10 条 | 编号列表展示,让用户选编号。每项给出 name、tags、description 摘要、published 状态。 |
|
||||
| 10–30 条 | 先展示 tag 分布帮助用户进一步收紧:列出匹配结果中最常见的子标签,提示"加一个 tag 可将结果缩小到 N 条"。 |
|
||||
| >30 条 | 强制要求用户补充条件:仅 published、指定具体 tag 组合、或按名称关键词过滤。 |
|
||||
|
||||
**确认 workflow 后**:
|
||||
|
||||
1. 将 `workflow_uuid` 写入 session state
|
||||
2. 提示用户下一步可用的 skill:
|
||||
- 提交实验 → 引导到“与其他 Skill 的协作”
|
||||
- 查看 workflow 详细节点 → `GET /api/v1/lab/workflow/template/detail/<workflow_uuid>`
|
||||
3. 若用户想换一个,回到筛选步骤。
|
||||
|
||||
---
|
||||
|
||||
## 展示结果
|
||||
|
||||
推荐格式(表格 + 汇总统计):
|
||||
|
||||
```
|
||||
共 150 个工作流,其中 32 个匹配筛选条件 [tags: synthesis OR organic]
|
||||
|
||||
| UUID (短) | 名称 | Tags | 已发布 |
|
||||
|-----------|--------------------------|------------------------------|--------|
|
||||
| e0436638 | Synthesis v2 | synthesis, organic | ✓ |
|
||||
| 5b60dbb8 | Grignard Protocol | synthesis, organometallic | ✓ |
|
||||
| ... | ... | ... | ... |
|
||||
|
||||
所有可用标签(按频次):
|
||||
synthesis (12), organic (8), analysis (5), purification (4), ...
|
||||
```
|
||||
|
||||
如果用户下一步想执行某工作流 → 引导到“与其他 Skill 的协作”。
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: tags 为 null 的工作流要不要展示?
|
||||
|
||||
默认**不展示**在筛选结果中(因为无法按 tag 匹配)。但在 `--summary-only` 或无筛选条件时,这些工作流仍会计入总数,并在输出中单独列出"未打标签"计数。
|
||||
|
||||
### Q: 如何按名称/描述做模糊匹配?
|
||||
|
||||
脚本未内置,但可在 jq 中组合:
|
||||
|
||||
```bash
|
||||
jq '[.[] | select((.name + " " + (.description // "")) | test("organic"; "i"))]' all_workflows.json
|
||||
```
|
||||
|
||||
### Q: `page_size=1000` 是否会被服务端限制?
|
||||
|
||||
接口通常允许最大 1000;如果返回量少于 1000 且 `has_more=false`,说明已到末页。极端情况下若服务端返回错误,可降到 200 或 500 再试。
|
||||
|
||||
### Q: 工作流数量极大(>10k)怎么办?
|
||||
|
||||
1. 先跑 `--summary-only` 了解 tag 分布
|
||||
2. 提示用户先限定 `--published-only` 或指定 tag
|
||||
3. 考虑将 `all_workflows.json` 缓存到本地,下次直接复用
|
||||
|
||||
---
|
||||
|
||||
## 与其他 Skill 的协作
|
||||
|
||||
- 正常情况下,找到 workflow 之后可以直接用它提交实验(启动工作流的 api 端点 POST $BASE/api/v1/lab/workflow/<workflow_uuid>/run,不用别的 skill)
|
||||
- **仅当需要进行多次实验时,使用 batch-submit-experiment** — 筛选到目标工作流后,`workflow_uuid` 直接用于实验提交
|
||||
|
||||
## 脚本依赖
|
||||
|
||||
`scripts/filter_workflows.py` 仅使用 Python 标准库(`urllib`、`json`、`argparse`),无需额外安装。
|
||||
191
.cursor/skills/filter-workflow-by-tags/scripts/filter_workflows.py
Executable file
191
.cursor/skills/filter-workflow-by-tags/scripts/filter_workflows.py
Executable file
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env python3
|
||||
"""分页拉取 Uni-Lab 工作流列表,汇总 tags 并按 tag 筛选。
|
||||
|
||||
使用示例:
|
||||
python filter_workflows.py \
|
||||
--auth <base64token> \
|
||||
--base https://leap-lab.test.bohrium.com \
|
||||
--lab-uuid a9059772-... \
|
||||
--tags synthesis organic --mode any
|
||||
|
||||
仅依赖 Python 标准库。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from collections import Counter
|
||||
|
||||
|
||||
def fetch_all_workflows(base: str, auth_token: str, lab_uuid: str, page_size: int = 1000) -> list[dict]:
|
||||
"""分页拉取所有 owner 工作流,直到 has_more=false。"""
|
||||
workflows: list[dict] = []
|
||||
page = 1
|
||||
while True:
|
||||
query = urllib.parse.urlencode(
|
||||
{"page": page, "page_size": page_size, "lab_uuid": lab_uuid}
|
||||
)
|
||||
url = f"{base.rstrip('/')}/api/v1/lab/workflow/owner/list?{query}"
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
headers={
|
||||
"Authorization": f"Lab {auth_token}",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
payload = json.loads(resp.read().decode("utf-8"))
|
||||
except urllib.error.HTTPError as e:
|
||||
sys.exit(f"[ERROR] HTTP {e.code} on page {page}: {e.read().decode('utf-8', 'ignore')}")
|
||||
except urllib.error.URLError as e:
|
||||
sys.exit(f"[ERROR] URL error on page {page}: {e.reason}")
|
||||
|
||||
if payload.get("code") != 0:
|
||||
sys.exit(f"[ERROR] API returned non-zero code: {payload}")
|
||||
|
||||
data = payload.get("data") or {}
|
||||
page_items = data.get("data") or []
|
||||
workflows.extend(page_items)
|
||||
|
||||
if not data.get("has_more"):
|
||||
break
|
||||
page += 1
|
||||
# 防御性兜底,避免接口异常导致无限循环
|
||||
if page > 1000:
|
||||
print(f"[WARN] page count exceeded 1000, stopping early", file=sys.stderr)
|
||||
break
|
||||
|
||||
return workflows
|
||||
|
||||
|
||||
def aggregate_tags(workflows: list[dict]) -> tuple[list[str], dict[str, int], int]:
|
||||
"""返回 (sorted_tags, tag_counts, untagged_count)。"""
|
||||
counter: Counter[str] = Counter()
|
||||
untagged = 0
|
||||
for wf in workflows:
|
||||
tags = wf.get("tags")
|
||||
if not tags:
|
||||
untagged += 1
|
||||
continue
|
||||
for t in tags:
|
||||
if isinstance(t, str) and t.strip():
|
||||
counter[t.strip()] += 1
|
||||
return sorted(counter.keys()), dict(counter), untagged
|
||||
|
||||
|
||||
def filter_workflows(
|
||||
workflows: list[dict],
|
||||
want_tags: list[str],
|
||||
mode: str,
|
||||
published_only: bool,
|
||||
) -> list[dict]:
|
||||
"""按 tag 筛选。mode 取值 any / all。"""
|
||||
want_set = {t.strip() for t in want_tags if t.strip()}
|
||||
out: list[dict] = []
|
||||
for wf in workflows:
|
||||
if published_only and not wf.get("published"):
|
||||
continue
|
||||
if not want_set:
|
||||
out.append(wf)
|
||||
continue
|
||||
tags = wf.get("tags") or []
|
||||
tag_set = {t for t in tags if isinstance(t, str)}
|
||||
if mode == "all":
|
||||
if want_set.issubset(tag_set):
|
||||
out.append(wf)
|
||||
else: # any
|
||||
if want_set & tag_set:
|
||||
out.append(wf)
|
||||
return out
|
||||
|
||||
|
||||
def project_workflow(wf: dict) -> dict:
|
||||
"""精简输出字段。"""
|
||||
return {
|
||||
"uuid": wf.get("uuid"),
|
||||
"name": wf.get("name"),
|
||||
"description": wf.get("description", ""),
|
||||
"tags": wf.get("tags") or [],
|
||||
"published": bool(wf.get("published")),
|
||||
"user_id": wf.get("user_id"),
|
||||
}
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
p = argparse.ArgumentParser(description="Fetch & filter Uni-Lab workflows by tags.")
|
||||
p.add_argument("--auth", required=True, help="Base64 token (the part after `Lab `).")
|
||||
p.add_argument("--base", required=True, help="Base URL, e.g. https://leap-lab.test.bohrium.com")
|
||||
p.add_argument("--lab-uuid", required=True, help="Lab UUID.")
|
||||
p.add_argument("--tags", nargs="*", default=[], help="Tags to filter by (space separated).")
|
||||
p.add_argument(
|
||||
"--mode",
|
||||
choices=["any", "all"],
|
||||
default="any",
|
||||
help="any: workflow contains at least one tag; all: workflow contains every tag.",
|
||||
)
|
||||
p.add_argument("--published-only", action="store_true", help="Only include published workflows.")
|
||||
p.add_argument("--page-size", type=int, default=1000, help="Page size, default 1000.")
|
||||
p.add_argument(
|
||||
"--summary-only",
|
||||
action="store_true",
|
||||
help="Print tag summary without applying filter (still fetches everything).",
|
||||
)
|
||||
p.add_argument("--output", help="Write JSON result to this path. If omitted, print to stdout.")
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
workflows = fetch_all_workflows(
|
||||
base=args.base,
|
||||
auth_token=args.auth,
|
||||
lab_uuid=args.lab_uuid,
|
||||
page_size=args.page_size,
|
||||
)
|
||||
sorted_tags, tag_counts, untagged = aggregate_tags(workflows)
|
||||
|
||||
if args.summary_only:
|
||||
result = {
|
||||
"total_workflows": len(workflows),
|
||||
"untagged_count": untagged,
|
||||
"tag_counts": tag_counts,
|
||||
"all_tags": sorted_tags,
|
||||
}
|
||||
else:
|
||||
filtered = filter_workflows(
|
||||
workflows,
|
||||
want_tags=args.tags,
|
||||
mode=args.mode,
|
||||
published_only=args.published_only,
|
||||
)
|
||||
result = {
|
||||
"total_workflows": len(workflows),
|
||||
"untagged_count": untagged,
|
||||
"tag_counts": tag_counts,
|
||||
"all_tags": sorted_tags,
|
||||
"filter": {
|
||||
"tags": args.tags,
|
||||
"mode": args.mode,
|
||||
"published_only": args.published_only,
|
||||
},
|
||||
"matched_count": len(filtered),
|
||||
"filtered_workflows": [project_workflow(wf) for wf in filtered],
|
||||
}
|
||||
|
||||
payload = json.dumps(result, ensure_ascii=False, indent=2)
|
||||
if args.output:
|
||||
with open(args.output, "w", encoding="utf-8") as f:
|
||||
f.write(payload)
|
||||
print(f"Wrote {len(workflows)} workflows summary → {args.output}", file=sys.stderr)
|
||||
else:
|
||||
print(payload)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
251
.cursor/skills/host-node/SKILL.md
Normal file
251
.cursor/skills/host-node/SKILL.md
Normal file
@@ -0,0 +1,251 @@
|
||||
---
|
||||
name: host-node
|
||||
description: Operate Uni-Lab host node via REST API — create resources, test latency, test resource tree, manual confirm. Use when the user mentions host_node, creating resources, resource management, testing latency, or any host node operation.
|
||||
---
|
||||
|
||||
# Host Node API Skill
|
||||
|
||||
## 设备信息
|
||||
|
||||
- **device_id**: `host_node`
|
||||
- **Python 源码**: `unilabos/ros/nodes/presets/host_node.py`
|
||||
- **设备类**: `HostNode`
|
||||
- **动作数**: 4(`create_resource`, `test_latency`, `auto-test_resource`, `manual_confirm`)
|
||||
|
||||
## 前置条件(缺一不可)
|
||||
|
||||
使用本 skill 前,**必须**先确认以下信息。如果缺少任何一项,**立即向用户询问并终止**,等补齐后再继续。
|
||||
|
||||
### 1. ak / sk → AUTH
|
||||
|
||||
从启动参数 `--ak` `--sk` 或 config.py 中获取,生成 token:`base64(ak:sk)` → `Authorization: Lab <token>`
|
||||
|
||||
### 2. --addr → BASE URL
|
||||
|
||||
| `--addr` 值 | BASE |
|
||||
| ------------ | ----------------------------------- |
|
||||
| `test` | `https://leap-lab.test.bohrium.com` |
|
||||
| `uat` | `https://leap-lab.uat.bohrium.com` |
|
||||
| `local` | `http://127.0.0.1:48197` |
|
||||
| 不传(默认) | `https://leap-lab.bohrium.com` |
|
||||
|
||||
确认后设置:
|
||||
|
||||
```bash
|
||||
BASE="<根据 addr 确定的 URL>"
|
||||
AUTH="Authorization: Lab <token>"
|
||||
```
|
||||
|
||||
**两项全部就绪后才可发起 API 请求。**
|
||||
|
||||
## Session State
|
||||
|
||||
在整个对话过程中,agent 需要记住以下状态,避免重复询问用户:
|
||||
|
||||
- `lab_uuid` — 实验室 UUID(首次通过 API #1 自动获取,**不需要问用户**)
|
||||
- `device_name` — `host_node`
|
||||
|
||||
## 请求约定
|
||||
|
||||
所有请求使用 `curl -s`,POST/PATCH/DELETE 需加 `Content-Type: application/json`。
|
||||
|
||||
> **Windows 平台**必须使用 `curl.exe`(而非 PowerShell 的 `curl` 别名)。
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### 1. 获取实验室信息(自动获取 lab_uuid)
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/edge/lab/info" -H "$AUTH"
|
||||
```
|
||||
|
||||
返回 `data.uuid` 为 `lab_uuid`,`data.name` 为 `lab_name`。
|
||||
|
||||
### 2. 创建工作流
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/workflow/owner" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"name":"<名称>","lab_uuid":"<lab_uuid>","description":"<描述>"}'
|
||||
```
|
||||
|
||||
返回 `data.uuid` 为 `workflow_uuid`。创建成功后告知用户链接:`$BASE/laboratory/$lab_uuid/workflow/$workflow_uuid`
|
||||
|
||||
### 3. 创建节点
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/edge/workflow/node" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"workflow_uuid":"<workflow_uuid>","resource_template_name":"host_node","node_template_name":"<action_name>"}'
|
||||
```
|
||||
|
||||
- `resource_template_name` 固定为 `host_node`
|
||||
- `node_template_name` — action 名称(如 `create_resource`, `test_latency`)
|
||||
|
||||
### 4. 删除节点
|
||||
|
||||
```bash
|
||||
curl -s -X DELETE "$BASE/api/v1/lab/workflow/nodes" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"node_uuids":["<uuid1>"],"workflow_uuid":"<workflow_uuid>"}'
|
||||
```
|
||||
|
||||
### 5. 更新节点参数
|
||||
|
||||
```bash
|
||||
curl -s -X PATCH "$BASE/api/v1/lab/workflow/node" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"workflow_uuid":"<wf_uuid>","uuid":"<node_uuid>","param":{...}}'
|
||||
```
|
||||
|
||||
`param` 直接使用创建节点返回的 `data.param` 结构,修改需要填入的字段值。参考 [action-index.md](action-index.md) 确定哪些字段是 Slot。
|
||||
|
||||
### 6. 查询节点 handles
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/workflow/node-handles" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"node_uuids":["<node_uuid_1>","<node_uuid_2>"]}'
|
||||
```
|
||||
|
||||
### 7. 批量创建边
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/workflow/edges" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"edges":[{"source_node_uuid":"<uuid>","target_node_uuid":"<uuid>","source_handle_uuid":"<uuid>","target_handle_uuid":"<uuid>"}]}'
|
||||
```
|
||||
|
||||
### 8. 启动工作流
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/workflow/<workflow_uuid>/run" -H "$AUTH"
|
||||
```
|
||||
|
||||
### 9. 运行设备单动作
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/mcp/run/action" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"lab_uuid":"<lab_uuid>","device_id":"host_node","action":"<action_name>","action_type":"<type>","param":{...}}'
|
||||
```
|
||||
|
||||
`param` 直接放 goal 里的属性,**不要**再包一层 `{"goal": {...}}`。
|
||||
|
||||
> **WARNING: `action_type` 必须正确,传错会导致任务永远卡住无法完成。** 从下表或 `actions/<name>.json` 的 `type` 字段获取。
|
||||
|
||||
#### action_type 速查表
|
||||
|
||||
| action | action_type |
|
||||
|--------|-------------|
|
||||
| `test_latency` | `UniLabJsonCommand` |
|
||||
| `create_resource` | `ResourceCreateFromOuterEasy` |
|
||||
| `auto-test_resource` | `UniLabJsonCommand` |
|
||||
| `manual_confirm` | `UniLabJsonCommand` |
|
||||
|
||||
### 10. 查询任务状态
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/mcp/task/<task_uuid>" -H "$AUTH"
|
||||
```
|
||||
|
||||
### 11. 运行工作流单节点
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/mcp/run/workflow/action" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"node_uuid":"<node_uuid>"}'
|
||||
```
|
||||
|
||||
### 12. 获取资源树(物料信息)
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/material/download/$lab_uuid" -H "$AUTH"
|
||||
```
|
||||
|
||||
注意 `lab_uuid` 在路径中。返回 `data.nodes[]` 含所有节点(设备 + 物料),每个节点含 `name`、`uuid`、`type`、`parent`。
|
||||
|
||||
### 13. 获取工作流模板详情
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/workflow/template/detail/$workflow_uuid" -H "$AUTH"
|
||||
```
|
||||
|
||||
> 必须使用 `/lab/workflow/template/detail/{uuid}`,其他路径会返回 404。
|
||||
|
||||
### 14. 按名称查询物料模板
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/material/template/by-name?lab_uuid=$lab_uuid&name=<template_name>" -H "$AUTH"
|
||||
```
|
||||
|
||||
返回 `data.uuid` 为 `res_template_uuid`,用于 API #15。
|
||||
|
||||
### 15. 创建物料节点
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/edge/material/node" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"res_template_uuid":"<uuid>","name":"<名称>","display_name":"<显示名>","parent_uuid":"<父节点uuid>","data":{...}}'
|
||||
```
|
||||
|
||||
### 16. 更新物料节点
|
||||
|
||||
```bash
|
||||
curl -s -X PUT "$BASE/api/v1/edge/material/node" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"uuid":"<节点uuid>","display_name":"<新名称>","data":{...}}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Placeholder Slot 填写规则
|
||||
|
||||
| `placeholder_keys` 值 | Slot 类型 | 填写格式 | 选取范围 |
|
||||
| --------------------- | ------------ | ----------------------------------------------------- | ---------------------- |
|
||||
| `unilabos_resources` | ResourceSlot | `{"id": "/path/name", "name": "name", "uuid": "xxx"}` | 仅物料节点(非设备) |
|
||||
| `unilabos_devices` | DeviceSlot | `"/parent/device_name"` | 仅设备节点(type=device) |
|
||||
| `unilabos_nodes` | NodeSlot | `"/parent/node_name"` | 所有节点(设备 + 物料) |
|
||||
| `unilabos_class` | ClassSlot | `"class_name"` | 注册表中已注册的资源类 |
|
||||
|
||||
### host_node 设备的 Slot 字段表
|
||||
|
||||
| Action | 字段 | Slot 类型 | 说明 |
|
||||
| ----------------- | ----------- | ------------ | ------------------------------ |
|
||||
| `create_resource` | `res_id` | ResourceSlot | 新资源路径(可填不存在的路径) |
|
||||
| `create_resource` | `device_id` | DeviceSlot | 归属设备 |
|
||||
| `create_resource` | `parent` | NodeSlot | 父节点路径 |
|
||||
| `create_resource` | `class_name`| ClassSlot | 资源类名如 `"container"` |
|
||||
| `auto-test_resource` | `resource` | ResourceSlot | 单个测试物料 |
|
||||
| `auto-test_resource` | `resources` | ResourceSlot | 测试物料数组 |
|
||||
| `auto-test_resource` | `device` | DeviceSlot | 测试设备 |
|
||||
| `auto-test_resource` | `devices` | DeviceSlot | 测试设备 |
|
||||
|
||||
---
|
||||
|
||||
## 渐进加载策略
|
||||
|
||||
1. **SKILL.md**(本文件)— API 端点 + session state 管理
|
||||
2. **[action-index.md](action-index.md)** — 按分类浏览 4 个动作的描述和核心参数
|
||||
3. **[actions/\<name\>.json](actions/)** — 仅在需要构建具体请求时,加载对应 action 的完整 JSON Schema
|
||||
|
||||
---
|
||||
|
||||
## 完整工作流 Checklist
|
||||
|
||||
```
|
||||
Task Progress:
|
||||
- [ ] Step 1: GET /edge/lab/info 获取 lab_uuid
|
||||
- [ ] Step 2: 获取资源树 (GET #12) → 记住可用物料
|
||||
- [ ] Step 3: 读 action-index.md 确定要用的 action 名
|
||||
- [ ] Step 4: 创建工作流 (POST #2) → 记住 workflow_uuid,告知用户链接
|
||||
- [ ] Step 5: 创建节点 (POST #3, resource_template_name=host_node) → 记住 node_uuid + data.param
|
||||
- [ ] Step 6: 根据 _unilabos_placeholder_info 和资源树,填写 data.param 中的 Slot 字段
|
||||
- [ ] Step 7: 更新节点参数 (PATCH #5)
|
||||
- [ ] Step 8: 查询节点 handles (POST #6) → 获取各节点的 handle_uuid
|
||||
- [ ] Step 9: 批量创建边 (POST #7) → 用 handle_uuid 连接节点
|
||||
- [ ] Step 10: 启动工作流 (POST #8) 或运行单节点 (POST #11)
|
||||
- [ ] Step 11: 查询任务状态 (GET #10) 确认完成
|
||||
```
|
||||
58
.cursor/skills/host-node/action-index.md
Normal file
58
.cursor/skills/host-node/action-index.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Action Index — host_node
|
||||
|
||||
4 个动作,按功能分类。每个动作的完整 JSON Schema 在 `actions/<name>.json`。
|
||||
|
||||
---
|
||||
|
||||
## 资源管理
|
||||
|
||||
### `create_resource`
|
||||
|
||||
在资源树中创建新资源(容器、物料等),支持指定位置、类型和初始液体
|
||||
|
||||
- **action_type**: `ResourceCreateFromOuterEasy`
|
||||
- **Schema**: [`actions/create_resource.json`](actions/create_resource.json)
|
||||
- **可选参数**: `res_id`, `device_id`, `class_name`, `parent`, `bind_locations`, `liquid_input_slot`, `liquid_type`, `liquid_volume`, `slot_on_deck`
|
||||
- **占位符字段**:
|
||||
- `res_id` — **ResourceSlot**(特例:目标物料可能尚不存在,直接填期望路径)
|
||||
- `device_id` — **DeviceSlot**,填路径字符串如 `"/host_node"`
|
||||
- `parent` — **NodeSlot**,填路径字符串如 `"/workstation/deck"`
|
||||
- `class_name` — **ClassSlot**,填类名如 `"container"`
|
||||
|
||||
### `auto-test_resource`
|
||||
|
||||
测试资源系统,返回当前资源树和设备列表
|
||||
|
||||
- **action_type**: `UniLabJsonCommand`
|
||||
- **Schema**: [`actions/test_resource.json`](actions/test_resource.json)
|
||||
- **可选参数**: `resource`, `resources`, `device`, `devices`
|
||||
- **占位符字段**:
|
||||
- `resource` — **ResourceSlot**,单个物料节点 `{id, name, uuid}`
|
||||
- `resources` — **ResourceSlot**,物料节点数组 `[{id, name, uuid}, ...]`
|
||||
- `device` — **DeviceSlot**,设备路径字符串
|
||||
- `devices` — **DeviceSlot**,设备路径字符串
|
||||
|
||||
---
|
||||
|
||||
## 系统工具
|
||||
|
||||
### `test_latency`
|
||||
|
||||
测试设备通信延迟,返回 RTT、时间差、任务延迟等指标
|
||||
|
||||
- **action_type**: `UniLabJsonCommand`
|
||||
- **Schema**: [`actions/test_latency.json`](actions/test_latency.json)
|
||||
- **参数**: 无(零参数调用)
|
||||
|
||||
---
|
||||
|
||||
## 人工确认
|
||||
|
||||
### `manual_confirm`
|
||||
|
||||
创建人工确认节点,等待用户手动确认后继续
|
||||
|
||||
- **action_type**: `UniLabJsonCommand`
|
||||
- **Schema**: [`actions/manual_confirm.json`](actions/manual_confirm.json)
|
||||
- **核心参数**: `timeout_seconds`(超时时间,秒), `assignee_user_ids`(指派用户 ID 列表)
|
||||
- **占位符字段**: `assignee_user_ids` — `unilabos_manual_confirm` 类型
|
||||
93
.cursor/skills/host-node/actions/create_resource.json
Normal file
93
.cursor/skills/host-node/actions/create_resource.json
Normal file
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"type": "ResourceCreateFromOuterEasy",
|
||||
"goal": {
|
||||
"res_id": "res_id",
|
||||
"class_name": "class_name",
|
||||
"parent": "parent",
|
||||
"device_id": "device_id",
|
||||
"bind_locations": "bind_locations",
|
||||
"liquid_input_slot": "liquid_input_slot[]",
|
||||
"liquid_type": "liquid_type[]",
|
||||
"liquid_volume": "liquid_volume[]",
|
||||
"slot_on_deck": "slot_on_deck"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"res_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"device_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"class_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"parent": {
|
||||
"type": "string"
|
||||
},
|
||||
"bind_locations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z"
|
||||
],
|
||||
"title": "bind_locations",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"liquid_input_slot": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"liquid_type": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"liquid_volume": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"slot_on_deck": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [],
|
||||
"_unilabos_placeholder_info": {
|
||||
"res_id": "unilabos_resources",
|
||||
"device_id": "unilabos_devices",
|
||||
"parent": "unilabos_nodes",
|
||||
"class_name": "unilabos_class"
|
||||
}
|
||||
},
|
||||
"goal_default": {},
|
||||
"placeholder_keys": {
|
||||
"res_id": "unilabos_resources",
|
||||
"device_id": "unilabos_devices",
|
||||
"parent": "unilabos_nodes",
|
||||
"class_name": "unilabos_class"
|
||||
}
|
||||
}
|
||||
32
.cursor/skills/host-node/actions/manual_confirm.json
Normal file
32
.cursor/skills/host-node/actions/manual_confirm.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"type": "UniLabJsonCommand",
|
||||
"goal": {
|
||||
"timeout_seconds": "timeout_seconds",
|
||||
"assignee_user_ids": "assignee_user_ids"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"timeout_seconds": {
|
||||
"type": "integer"
|
||||
},
|
||||
"assignee_user_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"timeout_seconds",
|
||||
"assignee_user_ids"
|
||||
],
|
||||
"_unilabos_placeholder_info": {
|
||||
"assignee_user_ids": "unilabos_manual_confirm"
|
||||
}
|
||||
},
|
||||
"goal_default": {},
|
||||
"placeholder_keys": {
|
||||
"assignee_user_ids": "unilabos_manual_confirm"
|
||||
}
|
||||
}
|
||||
11
.cursor/skills/host-node/actions/test_latency.json
Normal file
11
.cursor/skills/host-node/actions/test_latency.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"type": "UniLabJsonCommand",
|
||||
"goal": {},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
},
|
||||
"goal_default": {},
|
||||
"placeholder_keys": {}
|
||||
}
|
||||
255
.cursor/skills/host-node/actions/test_resource.json
Normal file
255
.cursor/skills/host-node/actions/test_resource.json
Normal file
@@ -0,0 +1,255 @@
|
||||
{
|
||||
"type": "UniLabJsonCommand",
|
||||
"goal": {
|
||||
"resource": "resource",
|
||||
"resources": "resources",
|
||||
"device": "device",
|
||||
"devices": "devices"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resource": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"sample_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"parent": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"pose": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"position": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z"
|
||||
],
|
||||
"title": "position",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"orientation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"w": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"w"
|
||||
],
|
||||
"title": "orientation",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"position",
|
||||
"orientation"
|
||||
],
|
||||
"title": "pose",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"config": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": "resource"
|
||||
},
|
||||
"resources": {
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"sample_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"parent": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"pose": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"position": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z"
|
||||
],
|
||||
"title": "position",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"orientation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"w": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"w"
|
||||
],
|
||||
"title": "orientation",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"position",
|
||||
"orientation"
|
||||
],
|
||||
"title": "pose",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"config": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": "resources"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"device": {
|
||||
"type": "string",
|
||||
"description": "device reference"
|
||||
},
|
||||
"devices": {
|
||||
"type": "string",
|
||||
"description": "device reference"
|
||||
}
|
||||
},
|
||||
"required": [],
|
||||
"_unilabos_placeholder_info": {
|
||||
"resource": "unilabos_resources",
|
||||
"resources": "unilabos_resources",
|
||||
"device": "unilabos_devices",
|
||||
"devices": "unilabos_devices"
|
||||
}
|
||||
},
|
||||
"goal_default": {},
|
||||
"placeholder_keys": {
|
||||
"resource": "unilabos_resources",
|
||||
"resources": "unilabos_resources",
|
||||
"device": "unilabos_devices",
|
||||
"devices": "unilabos_devices"
|
||||
}
|
||||
}
|
||||
284
.cursor/skills/submit-agent-result/SKILL.md
Normal file
284
.cursor/skills/submit-agent-result/SKILL.md
Normal file
@@ -0,0 +1,284 @@
|
||||
---
|
||||
name: submit-agent-result
|
||||
description: Submit historical experiment results (agent_result) to Uni-Lab cloud platform (leap-lab) notebook — read data files, assemble JSON payload, PUT to cloud API. Use when the user wants to submit experiment results, upload agent results, report experiment data, or mentions agent_result/实验结果/历史记录/notebook结果.
|
||||
---
|
||||
|
||||
# Uni-Lab 提交历史实验记录指南
|
||||
|
||||
通过 Uni-Lab 云端 API 向已创建的 notebook 提交实验结果数据(agent_result)。支持从 JSON / CSV 文件读取数据,整合后提交。
|
||||
|
||||
> **重要**:本指南中的 `Authorization: Lab <token>` 是 **Uni-Lab 平台专用的认证方式**,`Lab` 是 Uni-Lab 的 auth scheme 关键字,**不是** HTTP Basic 认证。请勿将其替换为 `Basic`。
|
||||
|
||||
## 前置条件(缺一不可)
|
||||
|
||||
使用本指南前,**必须**先确认以下信息。如果缺少任何一项,**立即向用户询问并终止**,等补齐后再继续。
|
||||
|
||||
### 1. ak / sk → AUTH
|
||||
|
||||
询问用户的启动参数,从 `--ak` `--sk` 或 config.py 中获取。
|
||||
|
||||
生成 AUTH token:
|
||||
|
||||
```bash
|
||||
# ⚠️ 注意:scheme 是 "Lab"(Uni-Lab 专用),不是 "Basic"
|
||||
python -c "import base64,sys; print(base64.b64encode(f'{sys.argv[1]}:{sys.argv[2]}'.encode()).decode())" <ak> <sk>
|
||||
```
|
||||
|
||||
输出即为 token 值,拼接为 `Authorization: Lab <token>`(`Lab` 是 Uni-Lab 平台 auth scheme,不可替换为 `Basic`)。
|
||||
|
||||
### 2. --addr → BASE URL
|
||||
|
||||
| `--addr` 值 | BASE |
|
||||
| ------------ | ----------------------------------- |
|
||||
| `test` | `https://leap-lab.test.bohrium.com` |
|
||||
| `uat` | `https://leap-lab.uat.bohrium.com` |
|
||||
| `local` | `http://127.0.0.1:48197` |
|
||||
| 不传(默认) | `https://leap-lab.bohrium.com` |
|
||||
|
||||
确认后设置:
|
||||
|
||||
```bash
|
||||
BASE="<根据 addr 确定的 URL>"
|
||||
# ⚠️ Auth scheme 必须是 "Lab"(Uni-Lab 专用),不是 "Basic"
|
||||
AUTH="Authorization: Lab <上面命令输出的 token>"
|
||||
```
|
||||
|
||||
### 3. notebook_uuid(**必须询问用户**)
|
||||
|
||||
**必须主动询问用户**:「请提供要提交结果的 notebook UUID。」
|
||||
|
||||
notebook_uuid 来自之前通过「批量提交实验」创建的实验批次,即 `POST /api/v1/lab/notebook` 返回的 `data.uuid`。
|
||||
|
||||
如果用户不记得,可提示:
|
||||
|
||||
- 查看之前的对话记录中创建 notebook 时返回的 UUID
|
||||
- 或通过平台页面查找对应的 notebook
|
||||
|
||||
**绝不能跳过此步骤,没有 notebook_uuid 无法提交。**
|
||||
|
||||
### 4. 实验结果数据
|
||||
|
||||
用户需要提供实验结果数据,支持以下方式:
|
||||
|
||||
| 方式 | 说明 |
|
||||
| --------- | ----------------------------------------------- |
|
||||
| JSON 文件 | 直接作为 `agent_result` 的内容合并 |
|
||||
| CSV 文件 | 转为 `{"文件名": [行数据...]}` 格式 |
|
||||
| 手动指定 | 用户直接告知 key-value 数据,由 agent 构建 JSON |
|
||||
|
||||
**四项全部就绪后才可开始。**
|
||||
|
||||
## Session State
|
||||
|
||||
在整个对话过程中,agent 需要记住以下状态:
|
||||
|
||||
- `lab_uuid` — 实验室 UUID(通过 API #1 自动获取,**不需要问用户**)
|
||||
- `notebook_uuid` — 目标 notebook UUID(**必须询问用户**)
|
||||
|
||||
## 请求约定
|
||||
|
||||
所有请求使用 `curl -s`,PUT 需加 `Content-Type: application/json`。
|
||||
|
||||
> **Windows 平台**必须使用 `curl.exe`(而非 PowerShell 的 `curl` 别名),示例中的 `curl` 均指 `curl.exe`。
|
||||
>
|
||||
> **PowerShell JSON 传参**:PowerShell 中 `-d '{"key":"value"}'` 会因引号转义失败。请将 JSON 写入临时文件,用 `-d '@tmp_body.json'`(单引号包裹 `@`,否则 `@` 会被 PowerShell 解析为 splatting 运算符导致报错)。
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### 1. 获取实验室信息(自动获取 lab_uuid)
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/edge/lab/info" -H "$AUTH"
|
||||
```
|
||||
|
||||
返回:
|
||||
|
||||
```json
|
||||
{ "code": 0, "data": { "uuid": "xxx", "name": "实验室名称" } }
|
||||
```
|
||||
|
||||
记住 `data.uuid` 为 `lab_uuid`。
|
||||
|
||||
### 2. 提交实验结果(agent_result)
|
||||
|
||||
```bash
|
||||
curl -s -X PUT "$BASE/api/v1/lab/notebook/agent-result" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '<request_body>'
|
||||
```
|
||||
|
||||
请求体结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"notebook_uuid": "<notebook_uuid>",
|
||||
"agent_result": {
|
||||
"<key1>": "<value1>",
|
||||
"<key2>": 123,
|
||||
"<nested_key>": {"a": 1, "b": 2},
|
||||
"<array_key>": [{"col1": "v1", "col2": "v2"}, ...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **注意**:HTTP 方法是 **PUT**(不是 POST)。
|
||||
|
||||
#### 必要字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --------------- | ------------- | ------------------------------------------- |
|
||||
| `notebook_uuid` | string (UUID) | 目标 notebook 的 UUID,从批量提交实验时获取 |
|
||||
| `agent_result` | object | 实验结果数据,任意 JSON 对象 |
|
||||
|
||||
#### agent_result 内容格式
|
||||
|
||||
`agent_result` 接受**任意 JSON 对象**,常见格式:
|
||||
|
||||
**简单键值对**:
|
||||
|
||||
```json
|
||||
{
|
||||
"avg_rtt_ms": 12.5,
|
||||
"status": "success",
|
||||
"test_count": 5
|
||||
}
|
||||
```
|
||||
|
||||
**包含嵌套结构**:
|
||||
|
||||
```json
|
||||
{
|
||||
"summary": { "total": 100, "passed": 98, "failed": 2 },
|
||||
"measurements": [
|
||||
{ "sample_id": "S001", "value": 3.14, "unit": "mg/mL" },
|
||||
{ "sample_id": "S002", "value": 2.71, "unit": "mg/mL" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**从 CSV 文件导入**(脚本自动转换):
|
||||
|
||||
```json
|
||||
{
|
||||
"experiment_data": [
|
||||
{ "温度": 25, "压力": 101.3, "产率": 0.85 },
|
||||
{ "温度": 30, "压力": 101.3, "产率": 0.91 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 整合脚本
|
||||
|
||||
本文档同级目录下的 `scripts/prepare_agent_result.py` 可自动读取文件并构建请求体。
|
||||
|
||||
### 用法
|
||||
|
||||
```bash
|
||||
python scripts/prepare_agent_result.py \
|
||||
--notebook-uuid <uuid> \
|
||||
--files data1.json data2.csv \
|
||||
[--auth <token>] \
|
||||
[--base <BASE_URL>] \
|
||||
[--submit] \
|
||||
[--output <output.json>]
|
||||
```
|
||||
|
||||
| 参数 | 必选 | 说明 |
|
||||
| ----------------- | ---------- | ----------------------------------------------- |
|
||||
| `--notebook-uuid` | 是 | 目标 notebook UUID |
|
||||
| `--files` | 是 | 输入文件路径(支持多个,JSON / CSV) |
|
||||
| `--auth` | 提交时必选 | Lab token(base64(ak:sk)) |
|
||||
| `--base` | 提交时必选 | API base URL |
|
||||
| `--submit` | 否 | 加上此标志则直接提交到云端 |
|
||||
| `--output` | 否 | 输出 JSON 路径(默认 `agent_result_body.json`) |
|
||||
|
||||
### 文件合并规则
|
||||
|
||||
| 文件类型 | 合并方式 |
|
||||
| --------------------- | -------------------------------------------- |
|
||||
| `.json`(dict) | 字段直接合并到 `agent_result` 顶层 |
|
||||
| `.json`(list/other) | 以文件名为 key 放入 `agent_result` |
|
||||
| `.csv` | 以文件名(不含扩展名)为 key,值为行对象数组 |
|
||||
|
||||
多个文件的字段会合并。JSON dict 中的重复 key 后者覆盖前者。
|
||||
|
||||
### 示例
|
||||
|
||||
```bash
|
||||
# 仅生成请求体文件(不提交)
|
||||
python scripts/prepare_agent_result.py \
|
||||
--notebook-uuid 73c67dca-c8cc-4936-85a0-329106aa7cca \
|
||||
--files results.json measurements.csv
|
||||
|
||||
# 生成并直接提交
|
||||
python scripts/prepare_agent_result.py \
|
||||
--notebook-uuid 73c67dca-c8cc-4936-85a0-329106aa7cca \
|
||||
--files results.json \
|
||||
--auth YTFmZDlkNGUt... \
|
||||
--base https://leap-lab.test.bohrium.com \
|
||||
--submit
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 手动构建方式
|
||||
|
||||
如果不使用脚本,也可手动构建请求体:
|
||||
|
||||
1. 将实验结果数据组装为 JSON 对象
|
||||
2. 写入临时文件:
|
||||
|
||||
```json
|
||||
{
|
||||
"notebook_uuid": "<uuid>",
|
||||
"agent_result": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
3. 用 curl 提交:
|
||||
|
||||
```bash
|
||||
curl -s -X PUT "$BASE/api/v1/lab/notebook/agent-result" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '@tmp_body.json'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 完整工作流 Checklist
|
||||
|
||||
```
|
||||
Task Progress:
|
||||
- [ ] Step 1: 确认 ak/sk → 生成 AUTH token
|
||||
- [ ] Step 2: 确认 --addr → 设置 BASE URL
|
||||
- [ ] Step 3: GET /edge/lab/info → 获取 lab_uuid
|
||||
- [ ] Step 4: **询问用户** notebook_uuid(必须,不可跳过)
|
||||
- [ ] Step 5: 确认实验结果数据来源(文件路径或手动数据)
|
||||
- [ ] Step 6: 运行 prepare_agent_result.py 或手动构建请求体
|
||||
- [ ] Step 7: PUT /lab/notebook/agent-result 提交
|
||||
- [ ] Step 8: 检查返回结果,确认提交成功
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: notebook_uuid 从哪里获取?
|
||||
|
||||
从之前「批量提交实验」时 `POST /api/v1/lab/notebook` 的返回值 `data.uuid` 获取。也可以在平台 UI 中查找对应的 notebook。
|
||||
|
||||
### Q: agent_result 有固定的 schema 吗?
|
||||
|
||||
没有严格 schema,接受任意 JSON 对象。但建议包含有意义的字段名和结构化数据,方便后续分析。
|
||||
|
||||
### Q: 可以多次提交同一个 notebook 的结果吗?
|
||||
|
||||
可以,后续提交会覆盖之前的 agent_result。
|
||||
|
||||
### Q: 认证方式是 Lab 还是 Api?
|
||||
|
||||
本指南统一使用 `Authorization: Lab <base64(ak:sk)>` 方式(`Lab` 是 Uni-Lab 平台的 auth scheme,**绝不能用 `Basic` 替代**)。如果用户有独立的 API Key,也可用 `Authorization: Api <key>` 替代。
|
||||
@@ -0,0 +1,133 @@
|
||||
"""
|
||||
读取实验结果文件(JSON / CSV),整合为 agent_result 请求体并可选提交。
|
||||
|
||||
用法:
|
||||
python prepare_agent_result.py \
|
||||
--notebook-uuid <uuid> \
|
||||
--files data1.json data2.csv \
|
||||
[--auth <Lab token>] \
|
||||
[--base <BASE_URL>] \
|
||||
[--submit] \
|
||||
[--output <output.json>]
|
||||
|
||||
支持的输入文件格式:
|
||||
- .json → 直接作为 dict 合并
|
||||
- .csv → 转为 {"filename": [row_dict, ...]} 格式
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
|
||||
def read_json_file(filepath: str) -> Dict[str, Any]:
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def read_csv_file(filepath: str) -> List[Dict[str, Any]]:
|
||||
rows = []
|
||||
with open(filepath, "r", encoding="utf-8-sig") as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
converted = {}
|
||||
for k, v in row.items():
|
||||
try:
|
||||
converted[k] = int(v)
|
||||
except (ValueError, TypeError):
|
||||
try:
|
||||
converted[k] = float(v)
|
||||
except (ValueError, TypeError):
|
||||
converted[k] = v
|
||||
rows.append(converted)
|
||||
return rows
|
||||
|
||||
|
||||
def merge_files(filepaths: List[str]) -> Dict[str, Any]:
|
||||
"""将多个文件合并为一个 agent_result dict"""
|
||||
merged: Dict[str, Any] = {}
|
||||
for fp in filepaths:
|
||||
path = Path(fp)
|
||||
ext = path.suffix.lower()
|
||||
key = path.stem
|
||||
|
||||
if ext == ".json":
|
||||
data = read_json_file(fp)
|
||||
if isinstance(data, dict):
|
||||
merged.update(data)
|
||||
else:
|
||||
merged[key] = data
|
||||
elif ext == ".csv":
|
||||
merged[key] = read_csv_file(fp)
|
||||
else:
|
||||
print(f"[警告] 不支持的文件格式: {fp},跳过", file=sys.stderr)
|
||||
|
||||
return merged
|
||||
|
||||
|
||||
def build_request_body(notebook_uuid: str, agent_result: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return {
|
||||
"notebook_uuid": notebook_uuid,
|
||||
"agent_result": agent_result,
|
||||
}
|
||||
|
||||
|
||||
def submit(base: str, auth: str, body: Dict[str, Any]) -> Dict[str, Any]:
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
print("[错误] 提交需要 requests 库: pip install requests", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
url = f"{base}/api/v1/lab/notebook/agent-result"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Lab {auth}",
|
||||
}
|
||||
resp = requests.put(url, json=body, headers=headers, timeout=30)
|
||||
return {"status_code": resp.status_code, "body": resp.json() if resp.headers.get("content-type", "").startswith("application/json") else resp.text}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="整合实验结果文件并构建 agent_result 请求体")
|
||||
parser.add_argument("--notebook-uuid", required=True, help="目标 notebook UUID")
|
||||
parser.add_argument("--files", nargs="+", required=True, help="输入文件路径(JSON / CSV)")
|
||||
parser.add_argument("--auth", help="Lab token(base64(ak:sk))")
|
||||
parser.add_argument("--base", help="API base URL")
|
||||
parser.add_argument("--submit", action="store_true", help="直接提交到云端")
|
||||
parser.add_argument("--output", default="agent_result_body.json", help="输出 JSON 文件路径")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
for fp in args.files:
|
||||
if not os.path.exists(fp):
|
||||
print(f"[错误] 文件不存在: {fp}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
agent_result = merge_files(args.files)
|
||||
body = build_request_body(args.notebook_uuid, agent_result)
|
||||
|
||||
with open(args.output, "w", encoding="utf-8") as f:
|
||||
json.dump(body, f, ensure_ascii=False, indent=2)
|
||||
print(f"[完成] 请求体已保存: {args.output}")
|
||||
print(f" notebook_uuid: {args.notebook_uuid}")
|
||||
print(f" agent_result 字段数: {len(agent_result)}")
|
||||
print(f" 合并文件数: {len(args.files)}")
|
||||
|
||||
if args.submit:
|
||||
if not args.auth or not args.base:
|
||||
print("[错误] 提交需要 --auth 和 --base 参数", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print(f"\n[提交] PUT {args.base}/api/v1/lab/notebook/agent-result ...")
|
||||
result = submit(args.base, args.auth, body)
|
||||
print(f" HTTP {result['status_code']}")
|
||||
print(f" 响应: {json.dumps(result['body'], ensure_ascii=False)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
272
.cursor/skills/virtual-workbench/SKILL.md
Normal file
272
.cursor/skills/virtual-workbench/SKILL.md
Normal file
@@ -0,0 +1,272 @@
|
||||
---
|
||||
name: virtual-workbench
|
||||
description: Operate Virtual Workbench via REST API — prepare materials, move to heating stations, start heating, move to output, transfer resources. Use when the user mentions virtual workbench, virtual_workbench, 虚拟工作台, heating stations, material processing, or workbench operations.
|
||||
---
|
||||
|
||||
# Virtual Workbench API Skill
|
||||
|
||||
## 设备信息
|
||||
|
||||
- **device_id**: `virtual_workbench`
|
||||
- **Python 源码**: `unilabos/devices/virtual/workbench.py`
|
||||
- **设备类**: `VirtualWorkbench`
|
||||
- **动作数**: 6(`auto-prepare_materials`, `auto-move_to_heating_station`, `auto-start_heating`, `auto-move_to_output`, `transfer`, `manual_confirm`)
|
||||
- **设备描述**: 模拟工作台,包含 1 个机械臂(每次操作 2s,独占锁)和 3 个加热台(每次加热 60s,可并行)
|
||||
|
||||
### 典型工作流程
|
||||
|
||||
1. `prepare_materials` — 生成 A1-A5 物料(5 个 output handle)
|
||||
2. `move_to_heating_station` — 物料并发竞争机械臂,移动到空闲加热台
|
||||
3. `start_heating` — 启动加热(3 个加热台可并行)
|
||||
4. `move_to_output` — 加热完成后移到输出位置 Cn
|
||||
|
||||
## 前置条件(缺一不可)
|
||||
|
||||
使用本 skill 前,**必须**先确认以下信息。如果缺少任何一项,**立即向用户询问并终止**,等补齐后再继续。
|
||||
|
||||
### 1. ak / sk → AUTH
|
||||
|
||||
从启动参数 `--ak` `--sk` 或 config.py 中获取,生成 token:`base64(ak:sk)` → `Authorization: Lab <token>`
|
||||
|
||||
### 2. --addr → BASE URL
|
||||
|
||||
| `--addr` 值 | BASE |
|
||||
| ------------ | ----------------------------------- |
|
||||
| `test` | `https://leap-lab.test.bohrium.com` |
|
||||
| `uat` | `https://leap-lab.uat.bohrium.com` |
|
||||
| `local` | `http://127.0.0.1:48197` |
|
||||
| 不传(默认) | `https://leap-lab.bohrium.com` |
|
||||
|
||||
确认后设置:
|
||||
|
||||
```bash
|
||||
BASE="<根据 addr 确定的 URL>"
|
||||
AUTH="Authorization: Lab <token>"
|
||||
```
|
||||
|
||||
**两项全部就绪后才可发起 API 请求。**
|
||||
|
||||
## Session State
|
||||
|
||||
- `lab_uuid` — 实验室 UUID(首次通过 API #1 自动获取,**不需要问用户**)
|
||||
- `device_name` — `virtual_workbench`
|
||||
|
||||
## 请求约定
|
||||
|
||||
所有请求使用 `curl -s`,POST/PATCH/DELETE 需加 `Content-Type: application/json`。
|
||||
|
||||
> **Windows 平台**必须使用 `curl.exe`(而非 PowerShell 的 `curl` 别名)。
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### 1. 获取实验室信息(自动获取 lab_uuid)
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/edge/lab/info" -H "$AUTH"
|
||||
```
|
||||
|
||||
返回 `data.uuid` 为 `lab_uuid`,`data.name` 为 `lab_name`。
|
||||
|
||||
### 2. 创建工作流
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/workflow/owner" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"name":"<名称>","lab_uuid":"<lab_uuid>","description":"<描述>"}'
|
||||
```
|
||||
|
||||
返回 `data.uuid` 为 `workflow_uuid`。创建成功后告知用户链接:`$BASE/laboratory/$lab_uuid/workflow/$workflow_uuid`
|
||||
|
||||
### 3. 创建节点
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/edge/workflow/node" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"workflow_uuid":"<workflow_uuid>","resource_template_name":"virtual_workbench","node_template_name":"<action_name>"}'
|
||||
```
|
||||
|
||||
- `resource_template_name` 固定为 `virtual_workbench`
|
||||
- `node_template_name` — action 名称(如 `auto-prepare_materials`, `auto-move_to_heating_station`)
|
||||
|
||||
### 4. 删除节点
|
||||
|
||||
```bash
|
||||
curl -s -X DELETE "$BASE/api/v1/lab/workflow/nodes" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"node_uuids":["<uuid1>"],"workflow_uuid":"<workflow_uuid>"}'
|
||||
```
|
||||
|
||||
### 5. 更新节点参数
|
||||
|
||||
```bash
|
||||
curl -s -X PATCH "$BASE/api/v1/lab/workflow/node" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"workflow_uuid":"<wf_uuid>","uuid":"<node_uuid>","param":{...}}'
|
||||
```
|
||||
|
||||
参考 [action-index.md](action-index.md) 确定哪些字段是 Slot。
|
||||
|
||||
### 6. 查询节点 handles
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/workflow/node-handles" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"node_uuids":["<node_uuid_1>","<node_uuid_2>"]}'
|
||||
```
|
||||
|
||||
### 7. 批量创建边
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/workflow/edges" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"edges":[{"source_node_uuid":"<uuid>","target_node_uuid":"<uuid>","source_handle_uuid":"<uuid>","target_handle_uuid":"<uuid>"}]}'
|
||||
```
|
||||
|
||||
### 8. 启动工作流
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/workflow/<workflow_uuid>/run" -H "$AUTH"
|
||||
```
|
||||
|
||||
### 9. 运行设备单动作
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/mcp/run/action" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"lab_uuid":"<lab_uuid>","device_id":"virtual_workbench","action":"<action_name>","action_type":"<type>","param":{...}}'
|
||||
```
|
||||
|
||||
`param` 直接放 goal 里的属性,**不要**再包一层 `{"goal": {...}}`。
|
||||
|
||||
> **WARNING: `action_type` 必须正确,传错会导致任务永远卡住无法完成。** 从下表或 `actions/<name>.json` 的 `type` 字段获取。
|
||||
|
||||
#### action_type 速查表
|
||||
|
||||
| action | action_type |
|
||||
|--------|-------------|
|
||||
| `auto-prepare_materials` | `UniLabJsonCommand` |
|
||||
| `auto-move_to_heating_station` | `UniLabJsonCommand` |
|
||||
| `auto-start_heating` | `UniLabJsonCommand` |
|
||||
| `auto-move_to_output` | `UniLabJsonCommand` |
|
||||
| `transfer` | `UniLabJsonCommandAsync` |
|
||||
| `manual_confirm` | `UniLabJsonCommand` |
|
||||
|
||||
### 10. 查询任务状态
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/mcp/task/<task_uuid>" -H "$AUTH"
|
||||
```
|
||||
|
||||
### 11. 运行工作流单节点
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/mcp/run/workflow/action" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"node_uuid":"<node_uuid>"}'
|
||||
```
|
||||
|
||||
### 12. 获取资源树(物料信息)
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/material/download/$lab_uuid" -H "$AUTH"
|
||||
```
|
||||
|
||||
注意 `lab_uuid` 在路径中。返回 `data.nodes[]` 含所有节点(设备 + 物料),每个节点含 `name`、`uuid`、`type`、`parent`。
|
||||
|
||||
### 13. 获取工作流模板详情
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/workflow/template/detail/$workflow_uuid" -H "$AUTH"
|
||||
```
|
||||
|
||||
> 必须使用 `/lab/workflow/template/detail/{uuid}`,其他路径会返回 404。
|
||||
|
||||
### 14. 按名称查询物料模板
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/material/template/by-name?lab_uuid=$lab_uuid&name=<template_name>" -H "$AUTH"
|
||||
```
|
||||
|
||||
返回 `data.uuid` 为 `res_template_uuid`,用于 API #15。
|
||||
|
||||
### 15. 创建物料节点
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/edge/material/node" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"res_template_uuid":"<uuid>","name":"<名称>","display_name":"<显示名>","parent_uuid":"<父节点uuid>","data":{...}}'
|
||||
```
|
||||
|
||||
### 16. 更新物料节点
|
||||
|
||||
```bash
|
||||
curl -s -X PUT "$BASE/api/v1/edge/material/node" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"uuid":"<节点uuid>","display_name":"<新名称>","data":{...}}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Placeholder Slot 填写规则
|
||||
|
||||
| `placeholder_keys` 值 | Slot 类型 | 填写格式 | 选取范围 |
|
||||
| --------------------- | ------------ | ----------------------------------------------------- | ---------------------- |
|
||||
| `unilabos_resources` | ResourceSlot | `{"id": "/path/name", "name": "name", "uuid": "xxx"}` | 仅物料节点(非设备) |
|
||||
| `unilabos_devices` | DeviceSlot | `"/parent/device_name"` | 仅设备节点(type=device) |
|
||||
| `unilabos_nodes` | NodeSlot | `"/parent/node_name"` | 所有节点(设备 + 物料) |
|
||||
| `unilabos_class` | ClassSlot | `"class_name"` | 注册表中已注册的资源类 |
|
||||
|
||||
### virtual_workbench 设备的 Slot 字段表
|
||||
|
||||
| Action | 字段 | Slot 类型 | 说明 |
|
||||
| ----------------- | ---------------- | ------------ | -------------------- |
|
||||
| `transfer` | `resource` | ResourceSlot | 待转移物料数组 |
|
||||
| `transfer` | `target_device` | DeviceSlot | 目标设备路径 |
|
||||
| `transfer` | `mount_resource` | ResourceSlot | 目标孔位数组 |
|
||||
| `manual_confirm` | `resource` | ResourceSlot | 确认用物料数组 |
|
||||
| `manual_confirm` | `target_device` | DeviceSlot | 确认用目标设备 |
|
||||
| `manual_confirm` | `mount_resource` | ResourceSlot | 确认用目标孔位数组 |
|
||||
|
||||
> `prepare_materials`、`move_to_heating_station`、`start_heating`、`move_to_output` 这 4 个动作**无 Slot 字段**,参数为纯数值/整数。
|
||||
|
||||
---
|
||||
|
||||
## 渐进加载策略
|
||||
|
||||
1. **SKILL.md**(本文件)— API 端点 + session state 管理 + 设备工作流概览
|
||||
2. **[action-index.md](action-index.md)** — 按分类浏览 6 个动作的描述和核心参数
|
||||
3. **[actions/\<name\>.json](actions/)** — 仅在需要构建具体请求时,加载对应 action 的完整 JSON Schema
|
||||
|
||||
---
|
||||
|
||||
## 完整工作流 Checklist
|
||||
|
||||
```
|
||||
Task Progress:
|
||||
- [ ] Step 1: GET /edge/lab/info 获取 lab_uuid
|
||||
- [ ] Step 2: 获取资源树 (GET #12) → 记住可用物料
|
||||
- [ ] Step 3: 读 action-index.md 确定要用的 action 名
|
||||
- [ ] Step 4: 创建工作流 (POST #2) → 记住 workflow_uuid,告知用户链接
|
||||
- [ ] Step 5: 创建节点 (POST #3, resource_template_name=virtual_workbench) → 记住 node_uuid + data.param
|
||||
- [ ] Step 6: 根据 _unilabos_placeholder_info 和资源树,填写 data.param 中的 Slot 字段
|
||||
- [ ] Step 7: 更新节点参数 (PATCH #5)
|
||||
- [ ] Step 8: 查询节点 handles (POST #6) → 获取各节点的 handle_uuid
|
||||
- [ ] Step 9: 批量创建边 (POST #7) → 用 handle_uuid 连接节点
|
||||
- [ ] Step 10: 启动工作流 (POST #8) 或运行单节点 (POST #11)
|
||||
- [ ] Step 11: 查询任务状态 (GET #10) 确认完成
|
||||
```
|
||||
|
||||
### 典型 5 物料并发加热工作流示例
|
||||
|
||||
```
|
||||
prepare_materials (count=5)
|
||||
├─ channel_1 → move_to_heating_station (material_number=1) → start_heating → move_to_output
|
||||
├─ channel_2 → move_to_heating_station (material_number=2) → start_heating → move_to_output
|
||||
├─ channel_3 → move_to_heating_station (material_number=3) → start_heating → move_to_output
|
||||
├─ channel_4 → move_to_heating_station (material_number=4) → start_heating → move_to_output
|
||||
└─ channel_5 → move_to_heating_station (material_number=5) → start_heating → move_to_output
|
||||
```
|
||||
|
||||
创建节点时,`prepare_materials` 的 5 个 output handle(`channel_1` ~ `channel_5`)分别连接到 5 个 `move_to_heating_station` 节点的 `material_input` handle。每个 `move_to_heating_station` 的 `heating_station_output` 和 `material_number_output` 连接到对应 `start_heating` 的 `station_id_input` 和 `material_number_input`。
|
||||
76
.cursor/skills/virtual-workbench/action-index.md
Normal file
76
.cursor/skills/virtual-workbench/action-index.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Action Index — virtual_workbench
|
||||
|
||||
6 个动作,按功能分类。每个动作的完整 JSON Schema 在 `actions/<name>.json`。
|
||||
|
||||
---
|
||||
|
||||
## 物料准备
|
||||
|
||||
### `auto-prepare_materials`
|
||||
|
||||
批量准备物料(虚拟起始节点),生成 A1-A5 物料编号,输出 5 个 handle 供后续节点使用
|
||||
|
||||
- **action_type**: `UniLabJsonCommand`
|
||||
- **Schema**: [`actions/prepare_materials.json`](actions/prepare_materials.json)
|
||||
- **可选参数**: `count`(物料数量,默认 5)
|
||||
|
||||
---
|
||||
|
||||
## 机械臂 & 加热台操作
|
||||
|
||||
### `auto-move_to_heating_station`
|
||||
|
||||
将物料从 An 位置移动到空闲加热台(竞争机械臂,自动查找空闲加热台)
|
||||
|
||||
- **action_type**: `UniLabJsonCommand`
|
||||
- **Schema**: [`actions/move_to_heating_station.json`](actions/move_to_heating_station.json)
|
||||
- **核心参数**: `material_number`(物料编号,integer)
|
||||
|
||||
### `auto-start_heating`
|
||||
|
||||
启动指定加热台的加热程序(可并行,3 个加热台同时工作)
|
||||
|
||||
- **action_type**: `UniLabJsonCommand`
|
||||
- **Schema**: [`actions/start_heating.json`](actions/start_heating.json)
|
||||
- **核心参数**: `station_id`(加热台 ID),`material_number`(物料编号)
|
||||
|
||||
### `auto-move_to_output`
|
||||
|
||||
将加热完成的物料从加热台移动到输出位置 Cn
|
||||
|
||||
- **action_type**: `UniLabJsonCommand`
|
||||
- **Schema**: [`actions/move_to_output.json`](actions/move_to_output.json)
|
||||
- **核心参数**: `station_id`(加热台 ID),`material_number`(物料编号)
|
||||
|
||||
---
|
||||
|
||||
## 物料转移
|
||||
|
||||
### `transfer`
|
||||
|
||||
异步转移物料到目标设备(通过 ROS 资源转移)
|
||||
|
||||
- **action_type**: `UniLabJsonCommandAsync`
|
||||
- **Schema**: [`actions/transfer.json`](actions/transfer.json)
|
||||
- **核心参数**: `resource`, `target_device`, `mount_resource`
|
||||
- **占位符字段**:
|
||||
- `resource` — **ResourceSlot**,待转移的物料数组 `[{id, name, uuid}, ...]`
|
||||
- `target_device` — **DeviceSlot**,目标设备路径字符串
|
||||
- `mount_resource` — **ResourceSlot**,目标孔位数组 `[{id, name, uuid}, ...]`
|
||||
|
||||
---
|
||||
|
||||
## 人工确认
|
||||
|
||||
### `manual_confirm`
|
||||
|
||||
创建人工确认节点,等待用户手动确认后继续(含物料转移上下文)
|
||||
|
||||
- **action_type**: `UniLabJsonCommand`
|
||||
- **Schema**: [`actions/manual_confirm.json`](actions/manual_confirm.json)
|
||||
- **核心参数**: `resource`, `target_device`, `mount_resource`, `timeout_seconds`, `assignee_user_ids`
|
||||
- **占位符字段**:
|
||||
- `resource` — **ResourceSlot**,物料数组
|
||||
- `target_device` — **DeviceSlot**,目标设备路径
|
||||
- `mount_resource` — **ResourceSlot**,目标孔位数组
|
||||
- `assignee_user_ids` — `unilabos_manual_confirm` 类型
|
||||
270
.cursor/skills/virtual-workbench/actions/manual_confirm.json
Normal file
270
.cursor/skills/virtual-workbench/actions/manual_confirm.json
Normal file
@@ -0,0 +1,270 @@
|
||||
{
|
||||
"type": "UniLabJsonCommand",
|
||||
"goal": {
|
||||
"resource": "resource",
|
||||
"target_device": "target_device",
|
||||
"mount_resource": "mount_resource",
|
||||
"timeout_seconds": "timeout_seconds",
|
||||
"assignee_user_ids": "assignee_user_ids"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resource": {
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"sample_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"parent": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"pose": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"position": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z"
|
||||
],
|
||||
"title": "position",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"orientation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"w": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"w"
|
||||
],
|
||||
"title": "orientation",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"position",
|
||||
"orientation"
|
||||
],
|
||||
"title": "pose",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"config": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": "resource"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"target_device": {
|
||||
"type": "string",
|
||||
"description": "device reference"
|
||||
},
|
||||
"mount_resource": {
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"sample_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"parent": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"pose": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"position": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z"
|
||||
],
|
||||
"title": "position",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"orientation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"w": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"w"
|
||||
],
|
||||
"title": "orientation",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"position",
|
||||
"orientation"
|
||||
],
|
||||
"title": "pose",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"config": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": "mount_resource"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"timeout_seconds": {
|
||||
"type": "integer"
|
||||
},
|
||||
"assignee_user_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"resource",
|
||||
"target_device",
|
||||
"mount_resource",
|
||||
"timeout_seconds",
|
||||
"assignee_user_ids"
|
||||
],
|
||||
"_unilabos_placeholder_info": {
|
||||
"resource": "unilabos_resources",
|
||||
"target_device": "unilabos_devices",
|
||||
"mount_resource": "unilabos_resources",
|
||||
"assignee_user_ids": "unilabos_manual_confirm"
|
||||
}
|
||||
},
|
||||
"goal_default": {},
|
||||
"placeholder_keys": {
|
||||
"resource": "unilabos_resources",
|
||||
"target_device": "unilabos_devices",
|
||||
"mount_resource": "unilabos_resources",
|
||||
"assignee_user_ids": "unilabos_manual_confirm"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"type": "UniLabJsonCommand",
|
||||
"goal": {
|
||||
"material_number": "material_number"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"material_number": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"material_number"
|
||||
]
|
||||
},
|
||||
"goal_default": {},
|
||||
"placeholder_keys": {}
|
||||
}
|
||||
24
.cursor/skills/virtual-workbench/actions/move_to_output.json
Normal file
24
.cursor/skills/virtual-workbench/actions/move_to_output.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"type": "UniLabJsonCommand",
|
||||
"goal": {
|
||||
"station_id": "station_id",
|
||||
"material_number": "material_number"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"station_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"material_number": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"station_id",
|
||||
"material_number"
|
||||
]
|
||||
},
|
||||
"goal_default": {},
|
||||
"placeholder_keys": {}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"type": "UniLabJsonCommand",
|
||||
"goal": {
|
||||
"count": "count"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "integer",
|
||||
"default": 5
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"goal_default": {
|
||||
"count": 5
|
||||
},
|
||||
"placeholder_keys": {}
|
||||
}
|
||||
24
.cursor/skills/virtual-workbench/actions/start_heating.json
Normal file
24
.cursor/skills/virtual-workbench/actions/start_heating.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"type": "UniLabJsonCommand",
|
||||
"goal": {
|
||||
"station_id": "station_id",
|
||||
"material_number": "material_number"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"station_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"material_number": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"station_id",
|
||||
"material_number"
|
||||
]
|
||||
},
|
||||
"goal_default": {},
|
||||
"placeholder_keys": {}
|
||||
}
|
||||
255
.cursor/skills/virtual-workbench/actions/transfer.json
Normal file
255
.cursor/skills/virtual-workbench/actions/transfer.json
Normal file
@@ -0,0 +1,255 @@
|
||||
{
|
||||
"type": "UniLabJsonCommandAsync",
|
||||
"goal": {
|
||||
"resource": "resource",
|
||||
"target_device": "target_device",
|
||||
"mount_resource": "mount_resource"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resource": {
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"sample_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"parent": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"pose": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"position": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z"
|
||||
],
|
||||
"title": "position",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"orientation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"w": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"w"
|
||||
],
|
||||
"title": "orientation",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"position",
|
||||
"orientation"
|
||||
],
|
||||
"title": "pose",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"config": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": "resource"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"target_device": {
|
||||
"type": "string",
|
||||
"description": "device reference"
|
||||
},
|
||||
"mount_resource": {
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"sample_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"parent": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"pose": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"position": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z"
|
||||
],
|
||||
"title": "position",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"orientation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"w": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"w"
|
||||
],
|
||||
"title": "orientation",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"position",
|
||||
"orientation"
|
||||
],
|
||||
"title": "pose",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"config": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": "mount_resource"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"resource",
|
||||
"target_device",
|
||||
"mount_resource"
|
||||
],
|
||||
"_unilabos_placeholder_info": {
|
||||
"resource": "unilabos_resources",
|
||||
"target_device": "unilabos_devices",
|
||||
"mount_resource": "unilabos_resources"
|
||||
}
|
||||
},
|
||||
"goal_default": {},
|
||||
"placeholder_keys": {
|
||||
"resource": "unilabos_resources",
|
||||
"target_device": "unilabos_devices",
|
||||
"mount_resource": "unilabos_resources"
|
||||
}
|
||||
}
|
||||
483
.cursor/skills/yibin-electrolyte-submit/SKILL.md
Normal file
483
.cursor/skills/yibin-electrolyte-submit/SKILL.md
Normal file
@@ -0,0 +1,483 @@
|
||||
---
|
||||
name: yibin-electrolyte-submit
|
||||
description: >-
|
||||
通过 Uni-Lab Notebook API 向宜宾电解液工站提交实验,覆盖配液分液(Bioyond LIMS)、
|
||||
扣电组装(CoinCellAssembly)、扣电测试全流程。
|
||||
包含 Excel 解析、formulation 构建、工作流节点参数填写、notebook 提交与状态轮询。
|
||||
Use when the user wants to submit electrolyte experiments, assemble or test coin cells,
|
||||
parse experiment Excel files, build notebook payloads, or mentions
|
||||
宜宾/配液/分液/扣电/电解液实验/notebook提交/CoinCell/BioyondLIMS.
|
||||
---
|
||||
|
||||
# 宜宾电解液产线 API 操作指南
|
||||
|
||||
本 skill 覆盖两个设备的完整操作流程:
|
||||
1. **配液分液工站** (`bioyond_cell_workstation`) — Bioyond LIMS 配液/分液/转运
|
||||
2. **扣电组装站** (`BatteryStation`) — Modbus PLC 扣电组装/数据采集
|
||||
|
||||
## 设备信息
|
||||
|
||||
| 属性 | 配液分液工站 | 扣电组装站 |
|
||||
|------|------------|-----------|
|
||||
| device_id | `bioyond_cell_workstation` | `BatteryStation` |
|
||||
| 显示名 | 配液分液工站 | 扣电工作站 |
|
||||
| 源码 | `unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py` | `unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py` |
|
||||
| 类名 | `BioyondCellWorkstation` | `CoinCellAssemblyWorkstation` |
|
||||
| 通讯 | HTTP REST (Bioyond LIMS API) | Modbus TCP (PLC 寄存器) |
|
||||
|
||||
## 前置条件
|
||||
|
||||
### 认证信息
|
||||
|
||||
```
|
||||
AUTH="Authorization: Lab OTdlY2FkNmUtZmZmMi00YjhiLThhOWEtNWM5ODAyOTJmOTUxOmU0OGM2YWJkLTA4ZmEtNDFjMy04NzhhLTc4M2FiODlhZjYxMw=="
|
||||
BASE="https://uni-lab.test.bohrium.com"
|
||||
```
|
||||
|
||||
来源:`--ak 97ecad6e-fff2-4b8b-8a9a-5c980292f951 --sk e48c6abd-08fa-41c3-878a-783ab89af613 --addr test`
|
||||
|
||||
### 启动 unilab(云端模式)
|
||||
|
||||
> **重要**:提交实验前必须确保 unilab 正在运行且已连接云端 WebSocket。
|
||||
|
||||
```powershell
|
||||
$env:PYTHONIOENCODING="utf-8"
|
||||
conda activate newunilab2603
|
||||
cd D:\UniLabdev\Uni-Lab-OS\unilabos\devices\workstation
|
||||
unilab -g D:\UniLabdev\Uni-Lab-OS\yibin_electrolyte_config.json --ak 97ecad6e-fff2-4b8b-8a9a-5c980292f951 --sk e48c6abd-08fa-41c3-878a-783ab89af613 --upload_registry --addr test --disable_browser --skip_env_check
|
||||
```
|
||||
|
||||
**启动要点**:
|
||||
1. 必须先激活虚拟环境 `newunilab2603`
|
||||
2. 工作目录切到 `unilabos/devices/workstation`(设备驱动所在目录)
|
||||
3. `--upload_registry` 将 64 个设备 + 142 个资源注册到云端
|
||||
4. `--skip_env_check` + `PYTHONIOENCODING=utf-8` 避免 Windows GBK 编码崩溃
|
||||
5. 启动后后台运行,等待日志出现 `Application startup complete` 和 `Host node ready signal published with 3 devices`
|
||||
|
||||
**验证连接成功的标志**:
|
||||
- 日志出现 `[MessageProcessor] ... wss://uni-lab.test.bohrium.com/api/v1/ws/schedule`
|
||||
- 日志出现 `[WebSocketClient] Host node ready signal published with 3 devices`
|
||||
- 日志出现 `Resource tree add completed`(资源树同步完成)
|
||||
|
||||
### 云端物料上架与入库(启动后必做)
|
||||
|
||||
> **在提交实验之前,必须提醒用户完成以下云端操作,否则实验会因物料缺失而失败。**
|
||||
|
||||
1. **拖拽上料**:在云端 UI(`$BASE/laboratory/<lab_uuid>`)的资源树视图中,将物料拖拽到对应的仓库/库位上。unilab 启动后资源树会自动同步到云端,但物料的**上架位置**需要用户在 UI 上手动确认或调整。
|
||||
|
||||
2. **确认配液物料入库**:确保所有配液实验需要的试剂(如 LiPF6、EC、DMC、EMC 等)已在 LIMS 系统中完成入库。可通过以下方式验证:
|
||||
- 云端 UI 资源树中对应仓库(如"粉末加样头堆栈"、"配液站内试剂仓库")下有物料节点
|
||||
- 或通过 API #8 获取资源树后检查物料节点是否存在
|
||||
|
||||
3. **告知 AI 可以提交**:用户完成上述操作后,告知 AI "物料已上架,可以提交实验",AI 再执行 notebook 提交流程。
|
||||
|
||||
**提醒话术模板**(AI 应在启动成功后发送给用户):
|
||||
```
|
||||
unilab 已成功启动并连接云端。提交实验前请完成以下操作:
|
||||
1. 在云端 UI 上确认资源树中的物料位置,必要时拖拽调整上料位
|
||||
2. 确保配液所需的试剂(粉末、液体)已在 LIMS 中完成入库
|
||||
3. 完成后告诉我,我将为您提交实验
|
||||
```
|
||||
|
||||
### 生成 Action Schema(首次使用)
|
||||
|
||||
启动 unilab 后,在 `unilabos_data/` 目录下会生成 `req_device_registry_upload.json`。运行以下命令提取两个设备的 action JSON:
|
||||
|
||||
```bash
|
||||
python .cursor/skills/create-device-skill/scripts/extract_device_actions.py --registry unilabos_data/req_device_registry_upload.json bioyond_cell_workstation .cursor/skills/yibin-electrolyte-submit/actions/
|
||||
python .cursor/skills/create-device-skill/scripts/extract_device_actions.py --registry unilabos_data/req_device_registry_upload.json BatteryStation .cursor/skills/yibin-electrolyte-submit/actions/
|
||||
```
|
||||
|
||||
## 请求约定
|
||||
|
||||
- Windows 平台**必须用 `curl.exe`**(非 PowerShell 的 curl 别名)
|
||||
- 所有请求带 `$AUTH` 头
|
||||
- URL 格式:`$BASE/api/v1/<endpoint>`
|
||||
- POST/PATCH 请求体写入临时 JSON 文件后用 `-d '@tmp.json'` 传参(避免 PowerShell 转义问题)
|
||||
- 本地 API 基址:`http://127.0.0.1:8002/api/v1/`
|
||||
|
||||
## Session State
|
||||
|
||||
每次会话开始时,依次获取以下信息:
|
||||
|
||||
```bash
|
||||
# 1. lab_uuid
|
||||
curl.exe -s -X GET "$BASE/api/v1/edge/lab/info" -H "$AUTH"
|
||||
# → data.uuid → $lab_uuid
|
||||
|
||||
# 2. project_uuid
|
||||
curl.exe -s -X GET "$BASE/api/v1/lab/project/list?lab_uuid=$lab_uuid" -H "$AUTH"
|
||||
# → data.items[].uuid/name → 让用户选择或取唯一项 → $project_uuid
|
||||
```
|
||||
|
||||
## 工作流模板(重要)
|
||||
|
||||
> **必须向用户索要已有的工作流模板 UUID 或 URL,不要自行创建。**
|
||||
>
|
||||
> 原因:通过 `edge/workflow/node` API 创建节点会报 `resource_node_template not found`——
|
||||
> 云端的工作流节点模板系统和设备注册表是独立的,需要用户在云端 UI 上预先配置好工作流模板。
|
||||
|
||||
**获取方式**:
|
||||
- 用户提供工作流页面 URL,如 `$BASE/laboratory/<lab_uuid>/workflow/<workflow_uuid>`
|
||||
- 从 URL 中提取 `workflow_uuid`
|
||||
- 用 API 获取模板详情:
|
||||
|
||||
```
|
||||
GET /api/v1/lab/workflow/template/detail/<workflow_uuid>
|
||||
```
|
||||
|
||||
返回 `data.nodes[]`:每个节点的 uuid、name、param、device_name、handles、disabled。
|
||||
|
||||
**示例**:
|
||||
```
|
||||
工作流 URL: https://uni-lab.test.bohrium.com/laboratory/e9ed9102-d709-4741-b7a0-d1e8578e2065/workflow/b49f80d9-58d6-4456-a521-56f4dd39cda0
|
||||
→ workflow_uuid = b49f80d9-58d6-4456-a521-56f4dd39cda0
|
||||
```
|
||||
|
||||
从模板详情中提取**未 disabled** 的节点的 `uuid` 和 `name`,后续提交 notebook 时使用。
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### #1 获取 lab_uuid
|
||||
|
||||
```
|
||||
GET /api/v1/edge/lab/info
|
||||
```
|
||||
|
||||
### #2 列出项目
|
||||
|
||||
```
|
||||
GET /api/v1/lab/project/list?lab_uuid=$lab_uuid
|
||||
```
|
||||
|
||||
返回 `data.items[]`,取 `uuid` 和 `name`。
|
||||
|
||||
### #3 获取工作流模板详情
|
||||
|
||||
```
|
||||
GET /api/v1/lab/workflow/template/detail/<workflow_uuid>
|
||||
```
|
||||
|
||||
返回 `data.nodes[]`:每个节点的 uuid、name、param、device_name、handles。
|
||||
提取活跃节点(`disabled != true`)的 `uuid` 用于构建 notebook 请求。
|
||||
|
||||
### #4 提交实验(创建 notebook)— 核心 API
|
||||
|
||||
```
|
||||
POST /api/v1/lab/notebook
|
||||
Body: {
|
||||
"lab_uuid": "<lab_uuid>",
|
||||
"project_uuid": "<project_uuid>",
|
||||
"workflow_uuid": "<workflow_uuid>",
|
||||
"name": "<实验名称>",
|
||||
"node_params": [
|
||||
{
|
||||
"sample_uuids": [],
|
||||
"datas": [
|
||||
{
|
||||
"node_uuid": "<模板中的节点UUID>",
|
||||
"param": { <参数键值对> },
|
||||
"sample_params": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**关键注意事项**:
|
||||
- `node_params` 是数组,每个元素代表一轮实验
|
||||
- `datas` 中每个节点对应模板中的一个活跃节点
|
||||
- `param` 中的字段名**必须使用 Python 函数参数名**,不能用模板中存储的 LIMS 字段名(见下方映射表)
|
||||
|
||||
### #5 查询 notebook 状态
|
||||
|
||||
```
|
||||
GET /api/v1/lab/notebook/status?uuid=<notebook_uuid>
|
||||
```
|
||||
|
||||
| status | 含义 |
|
||||
|--------|------|
|
||||
| `running` | 执行中 |
|
||||
| `success` | 成功 |
|
||||
| `fail` | 失败 |
|
||||
|
||||
### #6 运行设备单动作(本地 API)
|
||||
|
||||
```
|
||||
POST http://127.0.0.1:8002/api/v1/job/add
|
||||
Body: {
|
||||
"device_id": "<device_id>",
|
||||
"action": "<action_name>",
|
||||
"action_args": { <参数键值对> },
|
||||
"sample_material": {}
|
||||
}
|
||||
```
|
||||
|
||||
本地 API 可自动解析 `action_type`,无需手动指定。适用于快速调试或云端未连接时。
|
||||
|
||||
### #7 查询本地任务状态
|
||||
|
||||
```
|
||||
GET http://127.0.0.1:8002/api/v1/job/<job_id>/status
|
||||
```
|
||||
|
||||
| status | 含义 |
|
||||
|--------|------|
|
||||
| 0 | UNKNOWN |
|
||||
| 1 | ACCEPTED |
|
||||
| 2 | EXECUTING |
|
||||
| 4 | SUCCEEDED |
|
||||
| 5 | CANCELED |
|
||||
| 6 | ABORTED |
|
||||
|
||||
### #8 获取资源树
|
||||
|
||||
```
|
||||
GET /api/v1/lab/material/download/<lab_uuid>
|
||||
```
|
||||
|
||||
返回所有节点(`id`, `name`, `uuid`, `type`, `parent`)。填写 Slot 字段时用此接口筛选节点。
|
||||
|
||||
## Placeholder Slot 填写规则
|
||||
|
||||
action JSON 中 `placeholder_keys` 标记了哪些字段需要填 Slot:
|
||||
|
||||
| placeholder 值 | Slot 类型 | 填写格式 |
|
||||
|---------------|-----------|---------|
|
||||
| `unilabos_resources` | ResourceSlot | `{"id": "/path/name", "name": "name", "uuid": "xxx"}` |
|
||||
| `unilabos_devices` | DeviceSlot | `"/parent/device_name"` 路径字符串 |
|
||||
| `unilabos_nodes` | NodeSlot | `"/parent/node_name"` 路径字符串 |
|
||||
| `unilabos_class` | ClassSlot | `"class_name"` 字符串 |
|
||||
| `unilabos_formulation` | FormulationSlot | `[{well_name, liquids: [{name, volume}]}]` |
|
||||
|
||||
### ResourceSlot 填写
|
||||
|
||||
从 API #8 资源树中筛选**物料**节点:
|
||||
|
||||
```json
|
||||
{"id": "/bioyond_cell_workstation/YB_Bioyond_Deck/自动堆栈-左", "name": "自动堆栈-左", "uuid": "3a19debc-..."}
|
||||
```
|
||||
|
||||
数组字段:`[{id, name, uuid}, ...]`
|
||||
特例:`create_resource` 的 `res_id` 允许填不存在的路径。
|
||||
|
||||
### DeviceSlot 填写
|
||||
|
||||
从资源树筛选 `type=device` 的节点,填路径字符串:
|
||||
|
||||
```
|
||||
"/BatteryStation"
|
||||
"/bioyond_cell_workstation"
|
||||
```
|
||||
|
||||
### FormulationSlot 填写
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"sample_uuid": "",
|
||||
"well_name": "YB_PrepBottle_15mL_Carrier_bottle_A1",
|
||||
"liquids": [
|
||||
{ "name": "LiPF6", "mass": 12.5 },
|
||||
{ "name": "EC", "mass": 50.0 }
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
`well_name` 从资源树中取物料节点的 `name`。
|
||||
|
||||
## 参数名映射(重要的坑)
|
||||
|
||||
> 工作流模板中存储的参数名和 Python 函数实际接受的参数名**不一定相同**。
|
||||
> 提交 notebook 时必须使用 **Python 函数参数名**。
|
||||
|
||||
### `create_orders_formulation` 参数映射
|
||||
|
||||
| 模板中的 param 键 | 实际 Python 参数名 | 说明 |
|
||||
|-------------------|-------------------|------|
|
||||
| `pouch_cell_info` | `pouch_cell_volume` | 软包组装分液体积 (mL) |
|
||||
| `conductivity_info` | `conductivity_volume` | 电导测试分液体积 (mL) |
|
||||
| `load_shedding_info` | `coin_cell_volume` | 扣电组装分液体积 (mL) |
|
||||
| `formulation` | `formulation` | 配方数组(名称一致) |
|
||||
| `batch_id` | `batch_id` | 批次号(名称一致) |
|
||||
| `bottle_type` | `bottle_type` | 配液瓶类型(名称一致) |
|
||||
| `mix_time` | `mix_time` | 混匀时间(秒)(名称一致) |
|
||||
| `conductivity_bottle_count` | `conductivity_bottle_count` | 电导瓶数(名称一致) |
|
||||
|
||||
当从模板中读到 `param` 包含 `pouch_cell_info` 等 LIMS 字段名时,提交 notebook 时要用右列的 Python 函数参数名。否则会报 `TypeError: got an unexpected keyword argument`。
|
||||
|
||||
## 典型工作流
|
||||
|
||||
### 方式一:通过 Notebook API 批量提交(推荐)
|
||||
|
||||
**适用场景**:多组配方的批量实验,云端管理实验记录
|
||||
|
||||
```
|
||||
1. 向用户索要工作流模板 URL(不要自行创建)
|
||||
2. 获取 lab_uuid(API #1)和 project_uuid(API #2)
|
||||
3. 获取工作流模板详情(API #3),提取活跃节点 UUID
|
||||
4. 解析用户提供的 Excel 文件,构建 formulation 数组
|
||||
5. 提交 notebook(API #4)
|
||||
6. 轮询 notebook 状态(API #5)直到完成
|
||||
```
|
||||
|
||||
**Excel 解析规则**:
|
||||
- 全局参数在第一个数据行:`batch_id`、`bottle_type`、`mix_time`、`coin_cell_volume`、`pouch_cell_volume`、`conductivity_volume`、`conductivity_bottle_count`
|
||||
- 配方列从"试剂名1"开始,交替排列:试剂名列 + 质量列(以 `(g)` 结尾)
|
||||
- 每行一个配方,`order_name` = 配方ID列
|
||||
- formulation 中每个配方的 materials 数组只包含 `mass > 0` 的试剂
|
||||
|
||||
**node_params 构建**:所有配方放入同一个 round 的同一个 datas 条目中,因为只有一个节点(`create_orders_formulation`)。
|
||||
|
||||
### 方式二:设备单步操作(本地 API)
|
||||
|
||||
**适用场景**:调试、快速测试
|
||||
|
||||
```
|
||||
1. 确保 unilab 已在本地启动
|
||||
2. 通过 POST http://127.0.0.1:8002/api/v1/job/add 提交任务
|
||||
3. 通过 GET /api/v1/job/<job_id>/status 查询状态
|
||||
```
|
||||
|
||||
### 设备操作流程:配液 → 转运 → 扣电
|
||||
|
||||
```
|
||||
1. [配液站] scheduler_start_and_auto_feeding → 启动调度 + 上料
|
||||
2. [配液站] create_orders_formulation → 创建配液实验(配方输入)
|
||||
3. [配液站] transfer_3_to_2_to_1_auto → 分液瓶板转运到扣电站
|
||||
4. [扣电站] func_pack_device_init_auto_start_combined → 初始化+自动+启动
|
||||
5. [扣电站] func_sendbottle_allpack_multi → 发送瓶数+批量组装
|
||||
```
|
||||
|
||||
## 云端使用心得
|
||||
|
||||
### 环境准备
|
||||
- Windows 必须设置 `$env:PYTHONIOENCODING="utf-8"` 防止编码崩溃
|
||||
- 使用 `--skip_env_check` 跳过依赖检查,加快启动
|
||||
- 工作目录建议在 `unilabos/devices/workstation` 下启动
|
||||
|
||||
### 连接与注册
|
||||
- `--upload_registry` 会自动将设备和资源注册到云端
|
||||
- WebSocket 连接建立后,本地和云端的资源树会自动同步
|
||||
- 注册成功后用户需在云端 UI 完成**物料拖放上架**操作
|
||||
- 如果 unilab 断开重连,资源树会重新同步
|
||||
|
||||
### 工作流模板
|
||||
- **不要自行调用 API 创建工作流或节点**——云端工作流节点模板需要预配置
|
||||
- 始终向用户索要已有的工作流模板 URL
|
||||
- 从 URL 中提取 `workflow_uuid`,通过 API #3 获取详情
|
||||
- 模板中 `disabled: true` 的节点跳过,只处理活跃节点
|
||||
|
||||
### Notebook 实验提交
|
||||
- Notebook 是云端管理实验的标准方式
|
||||
- 一个 notebook 可包含多轮(`node_params` 数组),每轮可包含多组参数
|
||||
- 提交后通过 API #5 轮询状态,LIMS 配液流程通常需要较长时间(8 个配方约 30-60 分钟)
|
||||
- 实验进度可在云端 UI 和本地 unilab 日志中同步查看
|
||||
|
||||
### 常见错误
|
||||
| 错误 | 原因 | 解决 |
|
||||
|------|------|------|
|
||||
| `edge not started error` | unilab 未连接云端 WebSocket | 检查 unilab 是否在运行、重启 |
|
||||
| `resource_node_template not found` | 云端没有该设备的工作流模板 | 向用户索要已有模板,不要自行创建 |
|
||||
| `got an unexpected keyword argument` | 参数名用了模板字段名而非 Python 函数参数名 | 参照上方映射表转换 |
|
||||
| `UnicodeEncodeError: 'gbk'` | Windows 默认编码不支持特殊字符 | 设置 `PYTHONIOENCODING=utf-8` |
|
||||
| `parse parameter error` | 云端 API 字段名错误 | `device_id` (非 `device_name`)、`action` (非 `action_name`)、必须带 `action_type` |
|
||||
|
||||
## 渐进加载策略
|
||||
|
||||
1. 先读本文件了解 API 端点、参数映射和云端注意事项
|
||||
2. 需要具体 action 参数时,读 [action-index.md](action-index.md) 查找 action 名称和核心参数
|
||||
3. 需要完整 schema 时,读 `actions/<action_name>.json`(需先运行提取命令生成)
|
||||
4. 需要理解参数含义时,读设备源码
|
||||
|
||||
## 完整 Notebook 提交 Checklist
|
||||
|
||||
```
|
||||
- [ ] 确认 unilab 已在本地启动并连接云端 WebSocket
|
||||
- [ ] 提醒用户在云端 UI 拖拽上料、确认物料位置
|
||||
- [ ] 提醒用户确认配液所需试剂已在 LIMS 完成入库
|
||||
- [ ] 等待用户确认物料就绪后再继续
|
||||
- [ ] 向用户索要工作流模板 URL → 提取 workflow_uuid
|
||||
- [ ] 获取 lab_uuid(API #1)
|
||||
- [ ] 获取 project_uuid(API #2)
|
||||
- [ ] 获取工作流模板详情(API #3),提取活跃节点 UUID
|
||||
- [ ] 解析用户 Excel 文件 → 构建 formulation + 全局参数
|
||||
- [ ] 注意参数名映射(模板字段名 → Python 函数参数名)
|
||||
- [ ] 提交 notebook(API #4)
|
||||
- [ ] 轮询 notebook 状态(API #5)直到完成
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 真实场景:宜宾产线 Excel 提交提示词模板
|
||||
|
||||
> 以下为已验证可用的标准提示词,适用于配液-分液-扣电全流程。
|
||||
|
||||
### 场景说明
|
||||
|
||||
- unilab 运行在本地 Windows 机器(miniforge 环境),连接云端 WebSocket
|
||||
- AI(Cursor / OpenClaw)在任意设备上,通过云端 API 操作,**不需要本地 127.0.0.1**
|
||||
- 工作流为 5 节点串联:`create_orders_formulation` → `transfer_3_to_2_to_1_auto` → `func_pack_device_init_auto_start_combined` → `func_sendbottle_allpack_multi` → `transfer_1_to_2`
|
||||
|
||||
### 已知固定参数(宜宾产线)
|
||||
|
||||
```
|
||||
BASE = https://uni-lab.test.bohrium.com
|
||||
lab_uuid = e9ed9102-d709-4741-b7a0-d1e8578e2065
|
||||
project = YiBinElectrolyte (bc5224b4-8120-4765-9961-9dfc1802a1f6)
|
||||
workflow = 配液分液formulation全流程 (2bc59938-db79-4415-ac2d-9897ef125f2f)
|
||||
```
|
||||
|
||||
#### 工作流节点 UUID(固定,无需重新查询)
|
||||
|
||||
| 顺序 | action | node_uuid |
|
||||
|------|--------|-----------|
|
||||
| Step1 | auto-create_orders_formulation | `ece6744a-81ac-4ae4-8cd1-1c8eeda1dab6` |
|
||||
| Step2 | auto-transfer_3_to_2_to_1_auto | `1c37a8dd-5ba0-413d-81db-94b9c936a171` |
|
||||
| Step3 | auto-func_pack_device_init_auto_start_combined | `97a676a2-d257-4479-9096-073b40300970` |
|
||||
| Step4 | auto-func_sendbottle_allpack_multi | `cf69017a-d29c-4aad-a63b-309d63dac2e9` |
|
||||
| Step5 | auto-transfer_1_to_2 | `80d1c1aa-dbc3-4601-86b7-5c22a992dd9e` |
|
||||
|
||||
### 标准提示词
|
||||
|
||||
```
|
||||
请使用 yibin-electrolyte-submit skill,提交以下实验:
|
||||
|
||||
工作流模板 URL:https://uni-lab.test.bohrium.com/laboratory/e9ed9102-d709-4741-b7a0-d1e8578e2065/workflow/2bc59938-db79-4415-ac2d-9897ef125f2f
|
||||
Excel 文件路径:<粘贴或上传 xlsx 路径>
|
||||
|
||||
注意事项:
|
||||
- lab_uuid、project_uuid、workflow节点UUID均已固定,无需重新查询
|
||||
- 直接解析 Excel → 构建 payload → 提交
|
||||
- mix_time 传标量整数即可(已兼容)
|
||||
- 试剂名以 Excel 为准,注意区分 LiDFOB / LiDOFB 等拼写
|
||||
- csv_export_path 取 Excel 中 csv_export_path 列的值
|
||||
- 提交后告知 notebook UUID,无需自动轮询(实验耗时较长)
|
||||
```
|
||||
|
||||
### Excel 列结构说明(experment_template_0415sim-*.xlsx)
|
||||
|
||||
| 列范围 | 内容 |
|
||||
|--------|------|
|
||||
| C | batch_id |
|
||||
| D | bottle_type |
|
||||
| E-H | coin_cell_volume / conductivity_bottle_count / conductivity_volume / csv_export_path |
|
||||
| I-T | 试剂名+质量 交替排列(最多6对)|
|
||||
| U | mix_time |
|
||||
| V | order_name(每行配方的订单号)|
|
||||
| W | pouch_cell_volume |
|
||||
| X-Y | target_device / target_location(Step2参数)|
|
||||
| AA | material_search_enable(Step3参数)|
|
||||
| AB-AS | 扣电站参数(Step4)|
|
||||
|
||||
### CSV 导出说明
|
||||
|
||||
每次 `create_orders_formulation` 完成后,在 `csv_export_path` 目录下生成:
|
||||
```
|
||||
electrolyte_orders_<YYYYMMDD_HHMMSS>.csv
|
||||
```
|
||||
列:`orderCode, orderName, 配液瓶类型, 配液瓶二维码, 分液瓶类型, 分液瓶二维码, 目标配液质量比, 真实配液质量比, 时间`
|
||||
|
||||
> **注意**:barCode 为 `null` 或 `"nullBarCode123456"` 是正常现象,表示 LIMS 中该物料尚未扫码。配液瓶缺失通常是因为物料未放在手动传递窗(`locationId` 前缀 `3a19deae-2c7a-`)。
|
||||
295
.cursor/skills/yibin-electrolyte-submit/action-index.md
Normal file
295
.cursor/skills/yibin-electrolyte-submit/action-index.md
Normal file
@@ -0,0 +1,295 @@
|
||||
# Action 索引
|
||||
|
||||
> Action JSON 文件需运行提取命令生成,详见 [SKILL.md](SKILL.md) 中「生成 Action Schema」。
|
||||
> 以下描述和参数信息基于源码分析。
|
||||
|
||||
---
|
||||
|
||||
## 配液分液工站 (`bioyond_cell_workstation`)
|
||||
|
||||
源码:`unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py`
|
||||
|
||||
### 调度控制
|
||||
|
||||
#### `scheduler_start`
|
||||
|
||||
启动 Bioyond LIMS 调度系统
|
||||
|
||||
- **核心参数**: 无(仅需 apiKey/requestTime,由设备内部处理)
|
||||
- **返回**: LIMS 响应 `{code, message, data}`
|
||||
|
||||
#### `scheduler_stop`
|
||||
|
||||
停止调度
|
||||
|
||||
- **核心参数**: 无
|
||||
|
||||
#### `scheduler_continue`
|
||||
|
||||
继续调度(从暂停状态恢复)
|
||||
|
||||
- **核心参数**: 无
|
||||
|
||||
#### `scheduler_reset`
|
||||
|
||||
复位调度
|
||||
|
||||
- **核心参数**: 无
|
||||
|
||||
#### `scheduler_start_and_auto_feeding`
|
||||
|
||||
**组合操作**:启动调度 + 自动化上料(4号→3号手套箱)
|
||||
|
||||
- **核心参数**: `xlsx_path`(Excel 物料模板路径,可选)
|
||||
- **可选参数**: WH4 加样头面 12 个点位(materialName + quantity)、WH4 原液瓶面 9 个点位(materialName + quantity + materialType + targetWH)、WH3 人工堆栈 15 个点位(materialType + materialId + quantity)
|
||||
- **流程**: 先 `scheduler_start()`,成功后执行 `auto_feeding4to3()`
|
||||
- **备注**: 支持 Excel 模式和手动参数模式,Excel 路径存在时优先使用 Excel
|
||||
|
||||
### 物料上料/下料
|
||||
|
||||
#### `auto_feeding4to3`
|
||||
|
||||
自动化上料:从 4 号手套箱转运物料到 3 号手套箱
|
||||
|
||||
- **核心参数**: `xlsx_path`(Excel 物料模板路径)
|
||||
- **可选参数**: 同 `scheduler_start_and_auto_feeding` 的 WH4/WH3 点位参数
|
||||
- **返回**: 等待上料任务完成后返回结果
|
||||
|
||||
#### `auto_batch_outbound_from_xlsx`
|
||||
|
||||
自动化下料(从 Excel 读取下料信息)
|
||||
|
||||
- **核心参数**: `xlsx_path`(Excel 下料模板)
|
||||
- **Excel 列**: locationId, warehouseId, 数量, x, y, z
|
||||
|
||||
### 物料管理
|
||||
|
||||
#### `create_and_inbound_materials`
|
||||
|
||||
批量创建固体物料并入库
|
||||
|
||||
- **核心参数**: `material_names`(物料名称列表,默认 `["LiPF6", "LiDFOB", "DTD", "LiFSI", "LiPO2F2"]`)
|
||||
- **可选参数**: `type_id`(物料类型ID), `warehouse_name`(目标仓库,默认 "粉末加样头堆栈")
|
||||
- **流程**: 创建物料 → 批量入库 → 同步
|
||||
|
||||
#### `create_material`
|
||||
|
||||
创建单个物料并可选入库
|
||||
|
||||
- **核心参数**: `material_name`, `type_id`, `warehouse_name`
|
||||
- **可选参数**: `location_name_or_id`(库位编号如 "A01" 或 UUID)
|
||||
|
||||
#### `create_sample`
|
||||
|
||||
创建配液板物料(含子瓶)并入库
|
||||
|
||||
- **核心参数**: `name`, `board_type`(如 "5ml分液瓶板"), `bottle_type`(如 "5ml分液瓶"), `location_code`(如 "A01")
|
||||
- **可选参数**: `warehouse_name`(默认 "手动堆栈")
|
||||
- **备注**: 自动创建 2x4=8 个子瓶
|
||||
|
||||
#### `storage_inbound`
|
||||
|
||||
单个物料入库
|
||||
|
||||
- **核心参数**: `material_id`, `location_id`
|
||||
|
||||
#### `storage_batch_inbound`
|
||||
|
||||
批量物料入库
|
||||
|
||||
- **核心参数**: `items`(`[{materialId, locationId}, ...]`)
|
||||
|
||||
### 配液实验
|
||||
|
||||
#### `create_orders`
|
||||
|
||||
从 Excel 文件创建配液实验订单
|
||||
|
||||
- **核心参数**: `xlsx_path`(Excel 文件路径)
|
||||
- **Excel 列**: 配方ID, 创建日期, 配液瓶类型, 混匀时间(s), 扣电组装分液体积, 软包组装分液体积, 电导测试分液体积, 电导测试分液瓶数, 以及所有以 `(g)` 结尾的物料列
|
||||
- **流程**: 解析 Excel → 提交订单 → 等待全部完成 → 计算质量比 → 提取分液瓶板 → 创建资源树对象
|
||||
- **返回**: `{status, total_orders, bottle_count, reports, mass_ratios, vial_plates}`
|
||||
|
||||
#### `create_orders_formulation`
|
||||
|
||||
从配方列表创建配液实验订单(前端/API 输入版本)
|
||||
|
||||
- **核心参数**: `formulation`(配方数组)
|
||||
- **可选参数**: `batch_id`, `bottle_type`(默认 "配液小瓶"), `mix_time`(秒,列表), `coin_cell_volume`, `pouch_cell_volume`, `conductivity_volume`, `conductivity_bottle_count`
|
||||
- **formulation 格式**:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"order_name": "配方A",
|
||||
"materials": [
|
||||
{"name": "LiPF6", "mass": 12.5},
|
||||
{"name": "EC", "mass": 50.0},
|
||||
{"name": "DMC", "mass": 37.5}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
- **返回**: 同 `create_orders`
|
||||
|
||||
### 物料转运
|
||||
|
||||
#### `transfer_3_to_2_to_1_auto`
|
||||
|
||||
**自动转运**:从 create_orders 结果中自动定位分液瓶板并转运到目标设备
|
||||
|
||||
- **核心参数**: `vial_plates`(分液瓶板列表,来自 create_orders 返回的 `vial_plates`)
|
||||
- **可选参数**: `target_device`(默认 "BatteryStation"), `target_location`(默认 "bottle_rack_6x2"), `mass_ratios`(配方信息)
|
||||
- **流程**: 遍历瓶板 → 解析 locationId → 调用 LIMS 转运 API → 更新资源树
|
||||
- **返回**: `{total, success, failed, results}`
|
||||
|
||||
#### `transfer_3_to_2_to_1`
|
||||
|
||||
3→2→1 物料转运(手动指定坐标)
|
||||
|
||||
- **核心参数**: `source_wh_id`, `source_x`, `source_y`, `source_z`
|
||||
|
||||
#### `transfer_3_to_2`
|
||||
|
||||
3→2 物料转运
|
||||
|
||||
- **核心参数**: `source_wh_id`, `source_x`, `source_y`, `source_z`
|
||||
|
||||
#### `transfer_1_to_2`
|
||||
|
||||
1→2 物料转运
|
||||
|
||||
- **核心参数**: 无
|
||||
|
||||
### 查询
|
||||
|
||||
#### `order_list_v2`
|
||||
|
||||
批量查询实验报告
|
||||
|
||||
- **可选参数**: `timeType`, `beginTime`, `endTime`, `status`(60=运行中, 80=完成, 90=失败), `filter`, `skipCount`, `pageCount`, `sorting`
|
||||
|
||||
---
|
||||
|
||||
## 扣电组装站 (`BatteryStation`)
|
||||
|
||||
源码:`unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py`
|
||||
|
||||
### 设备控制(组合操作)
|
||||
|
||||
#### `func_pack_device_init_auto_start_combined`
|
||||
|
||||
**组合操作**:设备初始化 → 物料搜寻确认 → 切换自动模式 → 启动
|
||||
|
||||
- **核心参数**: `material_search_enable`(是否启用物料搜寻,默认 `False`)
|
||||
- **前置检查**: REG_UNILAB_INTERACT=False, COIL_GB_L_IGNORE_CMD=False, 所有握手寄存器无残留
|
||||
- **流程**: 手动模式 → 初始化命令 → 监测物料搜寻弹窗并自动处理 → 自动模式 → 启动
|
||||
- **返回**: `True`/`False`
|
||||
- **备注**: 第一次运行必须调用此函数;后续批次调用 `func_sendbottle_allpack_multi`
|
||||
|
||||
### 批量组装
|
||||
|
||||
#### `func_sendbottle_allpack_multi`
|
||||
|
||||
**发送瓶数 + 批量组装**(适用于第二批次及后续批次)
|
||||
|
||||
- **核心参数**: `elec_num`(电解液瓶数), `elec_use_num`(每瓶组装电池数), `elec_vol`(电解液吸液量 μL,默认 50)
|
||||
- **可选参数**:
|
||||
- 双滴模式:`dual_drop_mode`(bool), `dual_drop_first_volume`(μL), `dual_drop_suction_timing`(bool), `dual_drop_start_timing`(bool)
|
||||
- 组装参数:`assembly_type`(7=不用铝箔垫/8=用), `assembly_pressure`(N,默认 4200)
|
||||
- 物料参数:`fujipian_panshu`, `fujipian_juzhendianwei`, `gemopanshu`, `gemo_juzhendianwei`, `qiangtou_juzhendianwei`
|
||||
- 开关:`lvbodian`(铝箔垫片), `battery_pressure_mode`(压力模式), `battery_clean_ignore`(忽略清洁)
|
||||
- 其他:`file_path`(CSV保存路径), `formulations`(配方信息,用于CSV追溯)
|
||||
- **流程**: 发送瓶数触发物料搬运 → 设置PLC参数 → 循环(等待PLC请求→下发参数→读取电池数据→写入CSV→更新资源树)→ 完成握手
|
||||
- **返回**: `{success, total_batteries, batteries, summary}`
|
||||
- **备注**: 设备已初始化后直接调用;`formulations` 来自 create_orders 的 `mass_ratios`
|
||||
|
||||
#### `func_allpack_cmd`
|
||||
|
||||
全套组装(基础版本,含断点续传)
|
||||
|
||||
- **核心参数**: `elec_num`, `elec_use_num`, `elec_vol`, `assembly_type`, `assembly_pressure`, `file_path`
|
||||
- **返回**: `{success, total_batteries, batteries, summary}`
|
||||
|
||||
#### `func_allpack_cmd_simp`
|
||||
|
||||
增强版组装(含双滴模式 + 负极片/隔膜/枪头参数)
|
||||
|
||||
- **核心参数**: 同 `func_sendbottle_allpack_multi`
|
||||
- **备注**: 被 `func_sendbottle_allpack_multi` 内部调用
|
||||
|
||||
### 设备控制(单步操作)
|
||||
|
||||
#### `func_pack_device_init`
|
||||
|
||||
设备初始化(手动模式 → 初始化 → 复位标志)
|
||||
|
||||
#### `func_pack_device_auto`
|
||||
|
||||
切换自动模式
|
||||
|
||||
#### `func_pack_device_start`
|
||||
|
||||
启动设备
|
||||
|
||||
#### `func_pack_device_stop`
|
||||
|
||||
设备停止
|
||||
|
||||
#### `func_pack_send_bottle_num`
|
||||
|
||||
发送电解液瓶数(触发物料搬运)
|
||||
|
||||
- **核心参数**: `bottle_num`(瓶数)
|
||||
|
||||
### PLC 参数设置
|
||||
|
||||
#### `qiming_coin_cell_code`
|
||||
|
||||
设置组装物料参数
|
||||
|
||||
- **核心参数**: `fujipian_panshu`(负极片盘数)
|
||||
- **可选参数**: `fujipian_juzhendianwei`, `gemopanshu`, `gemo_juzhendianwei`, `lvbodian`, `battery_pressure_mode`, `battery_pressure`, `battery_clean_ignore`
|
||||
|
||||
### 数据采集
|
||||
|
||||
#### `func_read_data_and_output`
|
||||
|
||||
持续数据采集并导出 CSV(后台循环运行)
|
||||
|
||||
- **核心参数**: `file_path`(CSV 保存目录)
|
||||
- **采集字段**: 开路电压, 极片质量, 组装时间, 压制力, 电解液加注量, 电池类型, 电解液二维码, 电池二维码
|
||||
|
||||
#### `func_stop_read_data`
|
||||
|
||||
停止 CSV 数据采集
|
||||
|
||||
### 设备状态属性(只读)
|
||||
|
||||
| 属性 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `sys_status` | str | 设备状态(启动中/停止中/复位中/初始化中) |
|
||||
| `sys_mode` | str | 设备模式(手动/自动) |
|
||||
| `data_assembly_coin_cell_num` | int | 已完成电池数量 |
|
||||
| `data_assembly_time` | float | 单颗电池组装时间(秒) |
|
||||
| `data_open_circuit_voltage` | float | 开路电压(V) |
|
||||
| `data_pole_weight` | float | 正极片称重(g) |
|
||||
| `data_glove_box_pressure` | float | 手套箱压力(mbar) |
|
||||
| `data_glove_box_o2_content` | float | 手套箱氧含量(ppm) |
|
||||
| `data_glove_box_water_content` | float | 手套箱水含量(ppm) |
|
||||
| `data_coin_cell_code` | str | 电池二维码 |
|
||||
| `data_electrolyte_code` | str | 电解液二维码 |
|
||||
|
||||
---
|
||||
|
||||
## 配置参考
|
||||
|
||||
设备图文件 `yibin_electrolyte_config.json` 中的仓库映射(`warehouse_mapping`):
|
||||
|
||||
| 仓库名称 | 说明 | 典型操作 |
|
||||
|---------|------|---------|
|
||||
| 粉末加样头堆栈 | 20 个点位 (A01-T01) | `create_and_inbound_materials` 入库目标 |
|
||||
| 配液站内试剂仓库 | 9 个点位 (A01-C03) | 试剂存储 |
|
||||
| 自动堆栈-左 | 4 个点位 | 分液瓶板存放,`transfer_3_to_2_to_1_auto` 的源位置 |
|
||||
| 自动堆栈-右 | 4 个点位 | 分液瓶板存放 |
|
||||
| 手动传递窗左/右 | 各 15 个点位 | 人工上料/下料 |
|
||||
| 4号手套箱内部堆栈 | 12 个点位 | `auto_feeding4to3` 的源位置 |
|
||||
4
.github/workflows/ci-check.yml
vendored
4
.github/workflows/ci-check.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
- name: Install ROS dependencies, uv and unilabos-msgs
|
||||
run: |
|
||||
echo Installing ROS dependencies...
|
||||
mamba install -n check-env conda-forge::uv conda-forge::opencv robostack-staging::ros-humble-ros-core robostack-staging::ros-humble-action-msgs robostack-staging::ros-humble-std-msgs robostack-staging::ros-humble-geometry-msgs robostack-staging::ros-humble-control-msgs robostack-staging::ros-humble-nav2-msgs uni-lab::ros-humble-unilabos-msgs robostack-staging::ros-humble-cv-bridge robostack-staging::ros-humble-vision-opencv robostack-staging::ros-humble-tf-transformations robostack-staging::ros-humble-moveit-msgs robostack-staging::ros-humble-tf2-ros robostack-staging::ros-humble-tf2-ros-py conda-forge::transforms3d -c robostack-staging -c conda-forge -c uni-lab -y
|
||||
mamba install -n check-env --override-channels -c robostack-staging -c conda-forge -c uni-lab conda-forge::uv conda-forge::opencv robostack-staging::ros-humble-ros-core robostack-staging::ros-humble-action-msgs robostack-staging::ros-humble-std-msgs robostack-staging::ros-humble-geometry-msgs robostack-staging::ros-humble-control-msgs robostack-staging::ros-humble-nav2-msgs uni-lab::ros-humble-unilabos-msgs robostack-staging::ros-humble-cv-bridge robostack-staging::ros-humble-vision-opencv robostack-staging::ros-humble-tf-transformations robostack-staging::ros-humble-moveit-msgs robostack-staging::ros-humble-tf2-ros robostack-staging::ros-humble-tf2-ros-py conda-forge::transforms3d -y
|
||||
|
||||
- name: Install pip dependencies and unilabos
|
||||
run: |
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
uv pip uninstall enum34 || echo enum34 not installed, skipping
|
||||
uv pip install .
|
||||
|
||||
- name: Run check mode (complete_registry)
|
||||
- name: Run check mode (AST registry validation)
|
||||
run: |
|
||||
call conda activate check-env
|
||||
echo Running check mode...
|
||||
|
||||
77
.github/workflows/conda-pack-build.yml
vendored
77
.github/workflows/conda-pack-build.yml
vendored
@@ -1,6 +1,10 @@
|
||||
name: Build Conda-Pack Environment
|
||||
|
||||
on:
|
||||
# 在 UniLabOS Conda Build 成功上传后自动构建非全量 conda-pack
|
||||
workflow_run:
|
||||
workflows: ["UniLabOS Conda Build"]
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
@@ -21,6 +25,16 @@ on:
|
||||
|
||||
jobs:
|
||||
build-conda-pack:
|
||||
if: |
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(
|
||||
github.event_name == 'workflow_run' &&
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event == 'workflow_run'
|
||||
)
|
||||
env:
|
||||
BUILD_FULL: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.build_full == 'true' }}
|
||||
PACKAGE_REF: ${{ github.event.inputs.branch || github.event.workflow_run.head_sha || github.ref_name }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -54,7 +68,9 @@ jobs:
|
||||
id: should_build
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ -z "${{ github.event.inputs.platforms }}" ]]; then
|
||||
if [[ "${{ github.event_name }}" != "workflow_dispatch" ]]; then
|
||||
echo "should_build=true" >> $GITHUB_OUTPUT
|
||||
elif [[ -z "${{ github.event.inputs.platforms }}" ]]; then
|
||||
echo "should_build=true" >> $GITHUB_OUTPUT
|
||||
elif [[ "${{ github.event.inputs.platforms }}" == *"${{ matrix.platform }}"* ]]; then
|
||||
echo "should_build=true" >> $GITHUB_OUTPUT
|
||||
@@ -65,7 +81,7 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
ref: ${{ github.event.inputs.branch || github.event.workflow_run.head_sha || github.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Miniforge (with mamba)
|
||||
@@ -75,7 +91,7 @@ jobs:
|
||||
miniforge-version: latest
|
||||
use-mamba: true
|
||||
python-version: '3.11.14'
|
||||
channels: conda-forge,robostack-staging,uni-lab,defaults
|
||||
channels: conda-forge,robostack-staging,uni-lab
|
||||
channel-priority: flexible
|
||||
activate-environment: unilab
|
||||
auto-update-conda: false
|
||||
@@ -86,13 +102,13 @@ jobs:
|
||||
run: |
|
||||
echo Installing unilabos and dependencies to unilab environment...
|
||||
echo Using mamba for faster and more reliable dependency resolution...
|
||||
echo Build full: ${{ github.event.inputs.build_full }}
|
||||
if "${{ github.event.inputs.build_full }}"=="true" (
|
||||
echo Build full: ${{ env.BUILD_FULL }}
|
||||
if "${{ env.BUILD_FULL }}"=="true" (
|
||||
echo Installing unilabos-full ^(complete package^)...
|
||||
mamba install -n unilab uni-lab::unilabos-full conda-pack -c uni-lab -c robostack-staging -c conda-forge -y
|
||||
mamba install -n unilab --override-channels -c uni-lab -c robostack-staging -c conda-forge uni-lab::unilabos-full conda-pack zstandard -y
|
||||
) else (
|
||||
echo Installing unilabos ^(minimal package^)...
|
||||
mamba install -n unilab uni-lab::unilabos conda-pack -c uni-lab -c robostack-staging -c conda-forge -y
|
||||
mamba install -n unilab --override-channels -c uni-lab -c robostack-staging -c conda-forge uni-lab::unilabos conda-pack zstandard -y
|
||||
)
|
||||
|
||||
- name: Install conda-pack, unilabos and dependencies (Unix)
|
||||
@@ -101,13 +117,13 @@ jobs:
|
||||
run: |
|
||||
echo "Installing unilabos and dependencies to unilab environment..."
|
||||
echo "Using mamba for faster and more reliable dependency resolution..."
|
||||
echo "Build full: ${{ github.event.inputs.build_full }}"
|
||||
if [[ "${{ github.event.inputs.build_full }}" == "true" ]]; then
|
||||
echo "Build full: ${{ env.BUILD_FULL }}"
|
||||
if [[ "${{ env.BUILD_FULL }}" == "true" ]]; then
|
||||
echo "Installing unilabos-full (complete package)..."
|
||||
mamba install -n unilab uni-lab::unilabos-full conda-pack -c uni-lab -c robostack-staging -c conda-forge -y
|
||||
mamba install -n unilab --override-channels -c uni-lab -c robostack-staging -c conda-forge uni-lab::unilabos-full conda-pack zstandard -y
|
||||
else
|
||||
echo "Installing unilabos (minimal package)..."
|
||||
mamba install -n unilab uni-lab::unilabos conda-pack -c uni-lab -c robostack-staging -c conda-forge -y
|
||||
mamba install -n unilab --override-channels -c uni-lab -c robostack-staging -c conda-forge uni-lab::unilabos conda-pack zstandard -y
|
||||
fi
|
||||
|
||||
- name: Get latest ros-humble-unilabos-msgs version (Windows)
|
||||
@@ -134,27 +150,27 @@ jobs:
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64'
|
||||
run: |
|
||||
echo Checking for available ros-humble-unilabos-msgs versions...
|
||||
mamba search ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge || echo Search completed
|
||||
mamba search --override-channels -c uni-lab -c robostack-staging -c conda-forge ros-humble-unilabos-msgs || echo Search completed
|
||||
echo.
|
||||
echo Updating ros-humble-unilabos-msgs to latest version...
|
||||
mamba update -n unilab ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge -y || echo Already at latest version
|
||||
mamba update -n unilab --override-channels -c uni-lab -c robostack-staging -c conda-forge ros-humble-unilabos-msgs -y || echo Already at latest version
|
||||
|
||||
- name: Check for newer ros-humble-unilabos-msgs (Unix)
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Checking for available ros-humble-unilabos-msgs versions..."
|
||||
mamba search ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge || echo "Search completed"
|
||||
mamba search --override-channels -c uni-lab -c robostack-staging -c conda-forge ros-humble-unilabos-msgs || echo "Search completed"
|
||||
echo ""
|
||||
echo "Updating ros-humble-unilabos-msgs to latest version..."
|
||||
mamba update -n unilab ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge -y || echo "Already at latest version"
|
||||
mamba update -n unilab --override-channels -c uni-lab -c robostack-staging -c conda-forge ros-humble-unilabos-msgs -y || echo "Already at latest version"
|
||||
|
||||
- name: Install latest unilabos from source (Windows)
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64'
|
||||
run: |
|
||||
echo Uninstalling existing unilabos...
|
||||
mamba run -n unilab pip uninstall unilabos -y || echo unilabos not installed via pip
|
||||
echo Installing unilabos from source (branch: ${{ github.event.inputs.branch }})...
|
||||
echo Installing unilabos from source (ref: ${{ env.PACKAGE_REF }})...
|
||||
mamba run -n unilab pip install .
|
||||
echo Verifying installation...
|
||||
mamba run -n unilab pip show unilabos
|
||||
@@ -165,7 +181,7 @@ jobs:
|
||||
run: |
|
||||
echo "Uninstalling existing unilabos..."
|
||||
mamba run -n unilab pip uninstall unilabos -y || echo "unilabos not installed via pip"
|
||||
echo "Installing unilabos from source (branch: ${{ github.event.inputs.branch }})..."
|
||||
echo "Installing unilabos from source (ref: ${{ env.PACKAGE_REF }})..."
|
||||
mamba run -n unilab pip install .
|
||||
echo "Verifying installation..."
|
||||
mamba run -n unilab pip show unilabos
|
||||
@@ -226,7 +242,9 @@ jobs:
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64'
|
||||
run: |
|
||||
echo Packing unilab environment with conda-pack...
|
||||
mamba activate unilab && conda pack -n unilab -o unilab-env-${{ matrix.platform }}.tar.gz --ignore-missing-files
|
||||
for /f "delims=" %%i in ('mamba run -n unilab python -c "import os; print(os.environ['CONDA_PREFIX'])"') do set "UNILAB_PREFIX=%%i"
|
||||
echo Packing environment at: %UNILAB_PREFIX%
|
||||
mamba run -n unilab conda-pack -p "%UNILAB_PREFIX%" -o unilab-env-${{ matrix.platform }}.tar.gz --ignore-missing-files
|
||||
echo Pack file created:
|
||||
dir unilab-env-${{ matrix.platform }}.tar.gz
|
||||
|
||||
@@ -235,8 +253,9 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Packing unilab environment with conda-pack..."
|
||||
mamba install conda-pack -c conda-forge -y
|
||||
conda pack -n unilab -o unilab-env-${{ matrix.platform }}.tar.gz --ignore-missing-files
|
||||
UNILAB_PREFIX="$(mamba run -n unilab python -c 'import os; print(os.environ["CONDA_PREFIX"])')"
|
||||
echo "Packing environment at: $UNILAB_PREFIX"
|
||||
mamba run -n unilab conda-pack -p "$UNILAB_PREFIX" -o unilab-env-${{ matrix.platform }}.tar.gz --ignore-missing-files
|
||||
echo "Pack file created:"
|
||||
ls -lh unilab-env-${{ matrix.platform }}.tar.gz
|
||||
|
||||
@@ -267,7 +286,7 @@ jobs:
|
||||
|
||||
rem Create README using Python script
|
||||
echo Creating: README.txt
|
||||
python scripts\create_readme.py ${{ matrix.platform }} ${{ github.event.inputs.branch }} dist-package\README.txt
|
||||
python scripts\create_readme.py ${{ matrix.platform }} ${{ env.PACKAGE_REF }} dist-package\README.txt
|
||||
|
||||
echo.
|
||||
echo Distribution package contents:
|
||||
@@ -303,7 +322,7 @@ jobs:
|
||||
|
||||
# Create README using Python script
|
||||
echo "Creating: README.txt"
|
||||
python scripts/create_readme.py ${{ matrix.platform }} ${{ github.event.inputs.branch }} dist-package/README.txt
|
||||
python scripts/create_readme.py ${{ matrix.platform }} ${{ env.PACKAGE_REF }} dist-package/README.txt
|
||||
|
||||
echo ""
|
||||
echo "Distribution package contents:"
|
||||
@@ -314,7 +333,7 @@ jobs:
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: unilab-pack-${{ matrix.platform }}-${{ github.event.inputs.branch }}
|
||||
name: unilab-pack-${{ matrix.platform }}-${{ env.PACKAGE_REF }}
|
||||
path: dist-package/
|
||||
retention-days: 90
|
||||
if-no-files-found: error
|
||||
@@ -326,9 +345,9 @@ jobs:
|
||||
echo Build Summary
|
||||
echo ==========================================
|
||||
echo Platform: ${{ matrix.platform }}
|
||||
echo Branch: ${{ github.event.inputs.branch }}
|
||||
echo Branch: ${{ env.PACKAGE_REF }}
|
||||
echo Python version: 3.11.14
|
||||
if "${{ github.event.inputs.build_full }}"=="true" (
|
||||
if "${{ env.BUILD_FULL }}"=="true" (
|
||||
echo Package: unilabos-full ^(complete^)
|
||||
) else (
|
||||
echo Package: unilabos ^(minimal^)
|
||||
@@ -337,7 +356,7 @@ jobs:
|
||||
echo Distribution package contents:
|
||||
dir dist-package
|
||||
echo.
|
||||
echo Artifact name: unilab-pack-${{ matrix.platform }}-${{ github.event.inputs.branch }}
|
||||
echo Artifact name: unilab-pack-${{ matrix.platform }}-${{ env.PACKAGE_REF }}
|
||||
echo.
|
||||
echo After download, extract the ZIP and run:
|
||||
echo install_unilab.bat
|
||||
@@ -351,9 +370,9 @@ jobs:
|
||||
echo "Build Summary"
|
||||
echo "=========================================="
|
||||
echo "Platform: ${{ matrix.platform }}"
|
||||
echo "Branch: ${{ github.event.inputs.branch }}"
|
||||
echo "Branch: ${{ env.PACKAGE_REF }}"
|
||||
echo "Python version: 3.11.14"
|
||||
if [[ "${{ github.event.inputs.build_full }}" == "true" ]]; then
|
||||
if [[ "${{ env.BUILD_FULL }}" == "true" ]]; then
|
||||
echo "Package: unilabos-full (complete)"
|
||||
else
|
||||
echo "Package: unilabos (minimal)"
|
||||
@@ -362,7 +381,7 @@ jobs:
|
||||
echo "Distribution package contents:"
|
||||
ls -lh dist-package/
|
||||
echo ""
|
||||
echo "Artifact name: unilab-pack-${{ matrix.platform }}-${{ github.event.inputs.branch }}"
|
||||
echo "Artifact name: unilab-pack-${{ matrix.platform }}-${{ env.PACKAGE_REF }}"
|
||||
echo ""
|
||||
echo "After download:"
|
||||
echo " install_unilab.sh"
|
||||
|
||||
4
.github/workflows/deploy-docs.yml
vendored
4
.github/workflows/deploy-docs.yml
vendored
@@ -56,7 +56,7 @@ jobs:
|
||||
miniforge-version: latest
|
||||
use-mamba: true
|
||||
python-version: '3.11.14'
|
||||
channels: conda-forge,robostack-staging,uni-lab,defaults
|
||||
channels: conda-forge,robostack-staging,uni-lab
|
||||
channel-priority: flexible
|
||||
activate-environment: unilab
|
||||
auto-update-conda: false
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
run: |
|
||||
echo "Installing unilabos and dependencies to unilab environment..."
|
||||
echo "Using mamba for faster and more reliable dependency resolution..."
|
||||
mamba install -n unilab uni-lab::unilabos -c uni-lab -c robostack-staging -c conda-forge -y
|
||||
mamba install -n unilab --override-channels -c uni-lab -c robostack-staging -c conda-forge uni-lab::unilabos -y
|
||||
|
||||
- name: Install latest unilabos from source
|
||||
run: |
|
||||
|
||||
33
.github/workflows/multi-platform-build.yml
vendored
33
.github/workflows/multi-platform-build.yml
vendored
@@ -10,6 +10,9 @@ on:
|
||||
# 支持 tag 推送(不依赖 CI Check)
|
||||
push:
|
||||
tags: ['v*']
|
||||
# GitHub Release 发布时自动构建并上传
|
||||
release:
|
||||
types: [published]
|
||||
# 手动触发
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
@@ -80,7 +83,7 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
# 如果是 workflow_run 触发,使用触发 CI Check 的 commit
|
||||
ref: ${{ github.event.workflow_run.head_sha || github.ref }}
|
||||
ref: ${{ github.event.workflow_run.head_sha || github.event.release.tag_name || github.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check if platform should be built
|
||||
@@ -96,12 +99,14 @@ jobs:
|
||||
echo "should_build=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Setup Miniconda
|
||||
- name: Setup Miniforge
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
uses: conda-incubator/setup-miniconda@v3
|
||||
with:
|
||||
miniconda-version: 'latest'
|
||||
channels: conda-forge,robostack-staging,defaults
|
||||
miniforge-version: latest
|
||||
use-mamba: true
|
||||
python-version: '3.11.14'
|
||||
channels: conda-forge,robostack-staging
|
||||
channel-priority: strict
|
||||
activate-environment: build-env
|
||||
auto-update-conda: false
|
||||
@@ -110,13 +115,15 @@ jobs:
|
||||
- name: Install rattler-build and anaconda-client
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
conda install -c conda-forge rattler-build anaconda-client
|
||||
mamba install -n build-env --override-channels -c conda-forge rattler-build anaconda-client -y
|
||||
|
||||
- name: Show environment info
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
conda info
|
||||
conda list | grep -E "(rattler-build|anaconda-client)"
|
||||
conda list -n build-env | grep -E "(rattler-build|anaconda-client)"
|
||||
conda run -n build-env rattler-build --version
|
||||
conda run -n build-env anaconda --version
|
||||
echo "Platform: ${{ matrix.platform }}"
|
||||
echo "OS: ${{ matrix.os }}"
|
||||
|
||||
@@ -124,9 +131,9 @@ jobs:
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
if [[ "${{ matrix.platform }}" == "osx-arm64" ]]; then
|
||||
rattler-build build -r ./recipes/msgs/recipe.yaml -c robostack -c robostack-staging -c conda-forge
|
||||
conda run -n build-env rattler-build build -r ./recipes/msgs/recipe.yaml -c robostack -c robostack-staging -c conda-forge
|
||||
else
|
||||
rattler-build build -r ./recipes/msgs/recipe.yaml -c robostack -c robostack-staging -c conda-forge
|
||||
conda run -n build-env rattler-build build -r ./recipes/msgs/recipe.yaml -c robostack -c robostack-staging -c conda-forge
|
||||
fi
|
||||
|
||||
- name: List built packages
|
||||
@@ -157,9 +164,15 @@ jobs:
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload to Anaconda.org (unilab organization)
|
||||
if: steps.should_build.outputs.should_build == 'true' && github.event.inputs.upload_to_anaconda == 'true'
|
||||
if: |
|
||||
steps.should_build.outputs.should_build == 'true' &&
|
||||
(
|
||||
github.event_name == 'release' ||
|
||||
startsWith(github.ref, 'refs/tags/') ||
|
||||
github.event.inputs.upload_to_anaconda == 'true'
|
||||
)
|
||||
run: |
|
||||
for package in $(find ./output -name "*.conda"); do
|
||||
echo "Uploading $package to unilab organization..."
|
||||
anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
|
||||
conda run -n build-env anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
|
||||
done
|
||||
|
||||
74
.github/workflows/unilabos-conda-build.yml
vendored
74
.github/workflows/unilabos-conda-build.yml
vendored
@@ -1,14 +1,10 @@
|
||||
name: UniLabOS Conda Build
|
||||
|
||||
on:
|
||||
# 在 CI Check 成功后自动触发
|
||||
# 在 Multi-Platform Conda Build 成功上传 msgs 后自动触发
|
||||
workflow_run:
|
||||
workflows: ["CI Check"]
|
||||
workflows: ["Multi-Platform Conda Build"]
|
||||
types: [completed]
|
||||
branches: [main, dev]
|
||||
# 标签推送时直接触发(发布版本)
|
||||
push:
|
||||
tags: ['v*']
|
||||
# 手动触发
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
@@ -33,30 +29,30 @@ on:
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
# 等待 CI Check 完成的 job (仅用于 workflow_run 触发)
|
||||
wait-for-ci:
|
||||
# 等待上游 msgs 构建完成的 job (仅用于 workflow_run 触发)
|
||||
wait-for-upstream:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'workflow_run'
|
||||
outputs:
|
||||
should_continue: ${{ steps.check.outputs.should_continue }}
|
||||
steps:
|
||||
- name: Check CI status
|
||||
- name: Check upstream workflow status
|
||||
id: check
|
||||
run: |
|
||||
if [[ "${{ github.event.workflow_run.conclusion }}" == "success" ]]; then
|
||||
if [[ "${{ github.event.workflow_run.conclusion }}" == "success" && ( "${{ github.event.workflow_run.event }}" == "release" || "${{ github.event.workflow_run.event }}" == "push" ) ]]; then
|
||||
echo "should_continue=true" >> $GITHUB_OUTPUT
|
||||
echo "CI Check passed, proceeding with build"
|
||||
echo "Multi-Platform Conda Build passed for release/tag, proceeding with UniLabOS build"
|
||||
else
|
||||
echo "should_continue=false" >> $GITHUB_OUTPUT
|
||||
echo "CI Check did not succeed (status: ${{ github.event.workflow_run.conclusion }}), skipping build"
|
||||
echo "Upstream workflow is not a successful release/tag build (status: ${{ github.event.workflow_run.conclusion }}, event: ${{ github.event.workflow_run.event }}), skipping build"
|
||||
fi
|
||||
|
||||
build:
|
||||
needs: [wait-for-ci]
|
||||
# 运行条件:workflow_run 触发且 CI 成功,或者其他触发方式
|
||||
needs: [wait-for-upstream]
|
||||
# 运行条件:workflow_run 触发且上游成功,或者手动触发
|
||||
if: |
|
||||
always() &&
|
||||
(needs.wait-for-ci.result == 'skipped' || needs.wait-for-ci.outputs.should_continue == 'true')
|
||||
(needs.wait-for-upstream.result == 'skipped' || needs.wait-for-upstream.outputs.should_continue == 'true')
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -79,7 +75,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
# 如果是 workflow_run 触发,使用触发 CI Check 的 commit
|
||||
# 如果是 workflow_run 触发,使用上游 conda 包构建的 commit
|
||||
ref: ${{ github.event.workflow_run.head_sha || github.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -96,12 +92,14 @@ jobs:
|
||||
echo "should_build=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Setup Miniconda
|
||||
- name: Setup Miniforge
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
uses: conda-incubator/setup-miniconda@v3
|
||||
with:
|
||||
miniconda-version: 'latest'
|
||||
channels: conda-forge,robostack-staging,uni-lab,defaults
|
||||
miniforge-version: latest
|
||||
use-mamba: true
|
||||
python-version: '3.11.14'
|
||||
channels: conda-forge,robostack-staging,uni-lab
|
||||
channel-priority: strict
|
||||
activate-environment: build-env
|
||||
auto-update-conda: false
|
||||
@@ -110,20 +108,22 @@ jobs:
|
||||
- name: Install rattler-build and anaconda-client
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
conda install -c conda-forge rattler-build anaconda-client
|
||||
mamba install -n build-env --override-channels -c conda-forge rattler-build anaconda-client -y
|
||||
|
||||
- name: Show environment info
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
conda info
|
||||
conda list | grep -E "(rattler-build|anaconda-client)"
|
||||
conda list -n build-env | grep -E "(rattler-build|anaconda-client)"
|
||||
conda run -n build-env rattler-build --version
|
||||
conda run -n build-env anaconda --version
|
||||
echo "Platform: ${{ matrix.platform }}"
|
||||
echo "OS: ${{ matrix.os }}"
|
||||
echo "Build full package: ${{ github.event.inputs.build_full || 'false' }}"
|
||||
echo "Build full package: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.build_full == 'true' }}"
|
||||
echo "Building packages:"
|
||||
echo " - unilabos-env (environment dependencies)"
|
||||
echo " - unilabos (with pip package)"
|
||||
if [[ "${{ github.event.inputs.build_full }}" == "true" ]]; then
|
||||
if [[ "${{ github.event_name == 'workflow_dispatch' && github.event.inputs.build_full == 'true' }}" == "true" ]]; then
|
||||
echo " - unilabos-full (complete package)"
|
||||
fi
|
||||
|
||||
@@ -131,14 +131,19 @@ jobs:
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
echo "Building unilabos-env (conda environment dependencies)..."
|
||||
rattler-build build -r .conda/environment/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge
|
||||
conda run -n build-env rattler-build build -r .conda/environment/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge
|
||||
|
||||
- name: Upload unilabos-env to Anaconda.org (if enabled)
|
||||
if: steps.should_build.outputs.should_build == 'true' && github.event.inputs.upload_to_anaconda == 'true'
|
||||
if: |
|
||||
steps.should_build.outputs.should_build == 'true' &&
|
||||
(
|
||||
github.event_name == 'workflow_run' ||
|
||||
github.event.inputs.upload_to_anaconda == 'true'
|
||||
)
|
||||
run: |
|
||||
echo "Uploading unilabos-env to uni-lab organization..."
|
||||
for package in $(find ./output -name "unilabos-env*.conda"); do
|
||||
anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
|
||||
conda run -n build-env anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
|
||||
done
|
||||
|
||||
- name: Build unilabos (with pip package)
|
||||
@@ -146,33 +151,40 @@ jobs:
|
||||
run: |
|
||||
echo "Building unilabos package..."
|
||||
# 如果已上传到 Anaconda,从 uni-lab channel 获取 unilabos-env;否则从本地 output 获取
|
||||
rattler-build build -r .conda/base/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge --channel ./output
|
||||
conda run -n build-env rattler-build build -r .conda/base/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge --channel ./output
|
||||
|
||||
- name: Upload unilabos to Anaconda.org (if enabled)
|
||||
if: steps.should_build.outputs.should_build == 'true' && github.event.inputs.upload_to_anaconda == 'true'
|
||||
if: |
|
||||
steps.should_build.outputs.should_build == 'true' &&
|
||||
(
|
||||
github.event_name == 'workflow_run' ||
|
||||
github.event.inputs.upload_to_anaconda == 'true'
|
||||
)
|
||||
run: |
|
||||
echo "Uploading unilabos to uni-lab organization..."
|
||||
for package in $(find ./output -name "unilabos-0*.conda" -o -name "unilabos-[0-9]*.conda"); do
|
||||
anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
|
||||
conda run -n build-env anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
|
||||
done
|
||||
|
||||
- name: Build unilabos-full - Only when explicitly requested
|
||||
if: |
|
||||
steps.should_build.outputs.should_build == 'true' &&
|
||||
github.event_name == 'workflow_dispatch' &&
|
||||
github.event.inputs.build_full == 'true'
|
||||
run: |
|
||||
echo "Building unilabos-full package on ${{ matrix.platform }}..."
|
||||
rattler-build build -r .conda/full/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge --channel ./output
|
||||
conda run -n build-env rattler-build build -r .conda/full/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge --channel ./output
|
||||
|
||||
- name: Upload unilabos-full to Anaconda.org (if enabled)
|
||||
if: |
|
||||
steps.should_build.outputs.should_build == 'true' &&
|
||||
github.event_name == 'workflow_dispatch' &&
|
||||
github.event.inputs.build_full == 'true' &&
|
||||
github.event.inputs.upload_to_anaconda == 'true'
|
||||
run: |
|
||||
echo "Uploading unilabos-full to uni-lab organization..."
|
||||
for package in $(find ./output -name "unilabos-full*.conda"); do
|
||||
anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
|
||||
conda run -n build-env anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
|
||||
done
|
||||
|
||||
- name: List built packages
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -5,6 +5,7 @@ output/
|
||||
unilabos_data/
|
||||
pyrightconfig.json
|
||||
.cursorignore
|
||||
device_package*/
|
||||
## Python
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
@@ -250,4 +251,7 @@ ros-humble-unilabos-msgs-0.9.13-h6403a04_5.tar.bz2
|
||||
*.bz2
|
||||
test_config.py
|
||||
|
||||
|
||||
# Local config files with secrets
|
||||
yibin_coin_cell_only_config.json
|
||||
yibin_electrolyte_config.json
|
||||
yibin_electrolyte_only_config.json
|
||||
|
||||
72
260415csv_export_walkthrough.md
Normal file
72
260415csv_export_walkthrough.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# CSV 导出功能变更概要
|
||||
|
||||
## 修改的文件
|
||||
|
||||
### 1. [bioyond_cell_workstation.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py)
|
||||
|
||||
#### 新增导入
|
||||
- `import csv` 和 `import os`(L14-15)
|
||||
|
||||
#### 新增方法
|
||||
|
||||
| 方法 | 功能 |
|
||||
|------|------|
|
||||
| `_extract_prep_bottle_from_report` | 从 order_finish 报文提取**配液瓶**信息(每订单最多1个) |
|
||||
| `_extract_vial_bottles_from_report` | 从 order_finish 报文提取**分液瓶**信息(每订单可多个,返回数组) |
|
||||
| `_export_order_csv` | 汇总所有信息写入 CSV 文件 |
|
||||
|
||||
#### 配液瓶筛选逻辑 (`_extract_prep_bottle_from_report`)
|
||||
- `typemode="1"`, `realQuantity=1`, `usedQuantity=1`
|
||||
- `locationId` 以 `3a19deae-2c7a-` 开头(手动传递窗)
|
||||
- LIMS API 二次确认:`typeName` 含"配液瓶(小)"或"配液瓶(大)"
|
||||
|
||||
#### 分液瓶筛选逻辑 (`_extract_vial_bottles_from_report`)
|
||||
- `typemode="1"`, `realQuantity=1`, `usedQuantity=1`
|
||||
- `locationId` 以 `3a19debc-84b5-` 或 `3a19debe-5200` 开头(自动堆栈-左/右)
|
||||
- LIMS API 二次确认:`typeName` 为"5ml分液瓶"或"20ml分液瓶"
|
||||
- **返回数组**,支持 1×5ml + n×20ml 的组合
|
||||
|
||||
#### 修改的方法
|
||||
|
||||
| 方法 | 变更 |
|
||||
|------|------|
|
||||
| `_submit_and_wait_orders` | 新增配液瓶+分液瓶提取步骤,将 `prep_bottles` 和 `vial_bottles` 存入 `final_result` |
|
||||
| `create_orders` | 添加 `csv_export_path` 参数,末尾调用 `_export_order_csv` |
|
||||
| `create_orders_formulation` | 添加 `csv_export_path` 参数,末尾调用 `_export_order_csv` |
|
||||
|
||||
#### CSV 输出格式
|
||||
```
|
||||
orderCode, orderName, 配液瓶类型, 配液瓶二维码, 分液瓶类型, 分液瓶二维码, 目标配液质量比, 真实配液质量比, 时间
|
||||
```
|
||||
- 单个分液瓶时直接写值;多个分液瓶时类型和二维码用 JSON 数组表示
|
||||
- CSV 编码使用 `utf-8-sig`(兼容 Excel 打开)
|
||||
- `csv_export_path` 默认为空字符串,不传则不导出(向后兼容)
|
||||
|
||||
---
|
||||
|
||||
### 2. [bioyond_cell.yaml](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/registry/devices/bioyond_cell.yaml)
|
||||
|
||||
为两个 action 注册了 `csv_export_path` 参数:
|
||||
|
||||
- `auto-create_orders`: `goal_default` + `schema.properties.goal.properties` 中添加 `csv_export_path`
|
||||
- `auto-create_orders_formulation`: 同上
|
||||
|
||||
---
|
||||
|
||||
### 3. [coin_cell_assembly.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py) 的 CSV 改动与全流程追溯
|
||||
|
||||
在 `bioyond_cell_workstation.py` 的 `_submit_and_wait_orders` 最后阶段,提取 `prep_bottles`(配液瓶)和 `vial_bottles`(分液瓶)的条码并随 `mass_ratios` 数组一起下发给各下游工站(例如扣电组装站),实现跨站的全流程配方追溯。
|
||||
|
||||
并在扣电站生成的 `date_xxx.csv` 中,**替换并新增**了以下列:
|
||||
- 移除了原有的 `formulation_order_code` 与合并的 `formulation_ratio` 列。
|
||||
- 新增 `orderName` 导出
|
||||
- 新增 `prep_bottle_barcode`(奔曜传递的配液瓶二维码)
|
||||
- 新增 `vial_bottle_barcodes`(奔曜传递的分液瓶二维码,多瓶时存 JSON 数组)
|
||||
- 新增 `target_mass_ratio` 理论目标质量比
|
||||
- 新增 `real_mass_ratio` 实际称量真实质量比
|
||||
|
||||
*注意:这与操作人员在手套箱内扫码传入扣电站的 `electrolyte_code` 是单独记录的,方便做数据核对。*
|
||||
|
||||
## 向后兼容性
|
||||
- `csv_export_path` 默认值为 `""`(空字符串),现有调用不受影响
|
||||
- 新增的 `prep_bottles` 和 `vial_bottles` 字段为 `final_result` 和 `mass_ratios` 内部的新增附属字段,不破坏现有数据结构。
|
||||
87
AGENTS.md
Normal file
87
AGENTS.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# AGENTS.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
Also follow the monorepo-level rules in `../AGENTS.md`.
|
||||
|
||||
## Build & Development
|
||||
|
||||
```bash
|
||||
# Install in editable mode (requires mamba env with python 3.11)
|
||||
pip install -e .
|
||||
uv pip install -r unilabos/utils/requirements.txt
|
||||
|
||||
# Run with a device graph
|
||||
unilab --graph <graph.json> --config <config.py> --backend ros
|
||||
unilab --graph <graph.json> --config <config.py> --backend simple # no ROS2 needed
|
||||
|
||||
# Common CLI flags
|
||||
unilab --app_bridges websocket fastapi # communication bridges
|
||||
unilab --test_mode # simulate hardware, no real execution
|
||||
unilab --check_mode # CI validation of registry imports
|
||||
unilab --skip_env_check # skip auto-install of dependencies
|
||||
unilab --visual rviz|web|disable # visualization mode
|
||||
unilab --is_slave # run as slave node
|
||||
|
||||
# Workflow upload subcommand
|
||||
unilab workflow_upload -f <workflow.json> -n <name> --tags tag1 tag2
|
||||
|
||||
# Tests
|
||||
pytest tests/ # all tests
|
||||
pytest tests/resources/test_resourcetreeset.py # single test file
|
||||
pytest tests/resources/test_resourcetreeset.py::TestClassName::test_method # single test
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Startup Flow
|
||||
|
||||
`unilab` CLI → `unilabos/app/main.py:main()` → loads config → builds registry → reads device graph (JSON/GraphML) → starts backend thread (ROS2/simple) → starts FastAPI web server + WebSocket client.
|
||||
|
||||
### Core Layers
|
||||
|
||||
**Registry** (`unilabos/registry/`): Singleton `Registry` class discovers and catalogs all device types, resource types, and communication devices from YAML definitions. Device types live in `registry/devices/*.yaml`, resources in `registry/resources/`, comms in `registry/device_comms/`. The registry resolves class paths to actual Python classes via `utils/import_manager.py`.
|
||||
|
||||
**Resource Tracking** (`unilabos/resources/resource_tracker.py`): Pydantic-based `ResourceDict` → `ResourceDictInstance` → `ResourceTreeSet` hierarchy. `ResourceTreeSet` is the canonical in-memory representation of all devices and resources, used throughout the system. Graph I/O is in `resources/graphio.py` (reads JSON/GraphML device topology files into `nx.Graph` + `ResourceTreeSet`).
|
||||
|
||||
**Device Drivers** (`unilabos/devices/`): 30+ hardware drivers organized by device type (liquid_handling, hplc, balance, arm, etc.). Each driver is a Python class that gets wrapped by `ros/device_node_wrapper.py:ros2_device_node()` to become a ROS2 node with publishers, subscribers, and action servers.
|
||||
|
||||
**ROS2 Layer** (`unilabos/ros/`): `device_node_wrapper.py` dynamically wraps any device class into `ROS2DeviceNode` (defined in `ros/nodes/base_device_node.py`). Preset node types in `ros/nodes/presets/` include `host_node`, `controller_node`, `workstation`, `serial_node`, `camera`. Messages use custom `unilabos_msgs` (pre-built, distributed via releases).
|
||||
|
||||
**Protocol Compilation** (`unilabos/compile/`): 20+ protocol compilers (add, centrifuge, dissolve, filter, heatchill, stir, pump, etc.) that transform YAML protocol definitions into executable sequences.
|
||||
|
||||
**Communication** (`unilabos/device_comms/`): Hardware communication adapters — OPC-UA client, Modbus PLC, RPC, and a universal driver. `app/communication.py` provides a factory pattern for WebSocket client connections to the cloud.
|
||||
|
||||
**Web/API** (`unilabos/app/web/`): FastAPI server with REST API (`api.py`), Jinja2 template pages (`pages.py`), and HTTP client for cloud communication (`client.py`). Runs on port 8002 by default.
|
||||
|
||||
### Configuration System
|
||||
|
||||
- **Config classes** in `unilabos/config/config.py`: `BasicConfig`, `WSConfig`, `HTTPConfig`, `ROSConfig` — all class-level attributes, loaded from Python config files
|
||||
- Config files are `.py` files with matching class names (see `config/example_config.py`)
|
||||
- Environment variables override with prefix `UNILABOS_` (e.g., `UNILABOS_BASICCONFIG_PORT=9000`)
|
||||
- Device topology defined in graph files (JSON with node-link format, or GraphML)
|
||||
|
||||
### Key Data Flow
|
||||
|
||||
1. Graph file → `graphio.read_node_link_json()` → `(nx.Graph, ResourceTreeSet, resource_links)`
|
||||
2. `ResourceTreeSet` + `Registry` → `initialize_device.initialize_device_from_dict()` → `ROS2DeviceNode` instances
|
||||
3. Device nodes communicate via ROS2 topics/actions or direct Python calls (simple backend)
|
||||
4. Cloud sync via WebSocket (`app/ws_client.py`) and HTTP (`app/web/client.py`)
|
||||
|
||||
### Test Data
|
||||
|
||||
Example device graphs and experiment configs are in `unilabos/test/experiments/` (not `tests/`). Registry test fixtures in `unilabos/test/registry/`.
|
||||
|
||||
## Code Conventions
|
||||
|
||||
- Code comments and log messages in simplified Chinese
|
||||
- Python 3.11+, type hints expected
|
||||
- Pydantic models for data validation (`resource_tracker.py`)
|
||||
- Singleton pattern via `@singleton` decorator (`utils/decorator.py`)
|
||||
- Dynamic class loading via `utils/import_manager.py` — device classes resolved at runtime from registry YAML paths
|
||||
- CLI argument dashes auto-converted to underscores for consistency
|
||||
|
||||
## Licensing
|
||||
|
||||
- Framework code: GPL-3.0
|
||||
- Device drivers (`unilabos/devices/`): DP Technology Proprietary License — do not redistribute
|
||||
168
CHANGES_2026_03_24.md
Normal file
168
CHANGES_2026_03_24.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# 变更说明 2026-03-24
|
||||
|
||||
## 问题背景
|
||||
|
||||
`BioyondElectrolyteDeck`(原 `BIOYOND_YB_Deck`)迁移后,前端物料未能正常上传/同步。
|
||||
|
||||
---
|
||||
|
||||
## 修复内容
|
||||
|
||||
### 1. `unilabos/resources/bioyond/decks.py`
|
||||
|
||||
- 补回 `setup: bool = False` 参数及 `if setup: self.setup()` 逻辑,与旧版 `BIOYOND_YB_Deck` 保持一致
|
||||
- 工厂函数 `bioyond_electrolyte_deck` 保留显式调用 `deck.setup()`,避免重复初始化
|
||||
|
||||
```python
|
||||
# 修复前(缺少 setup 参数,无法通过 setup=True 触发初始化)
|
||||
def __init__(self, name, size_x, size_y, size_z, category):
|
||||
super().__init__(...)
|
||||
|
||||
# 修复后
|
||||
def __init__(self, name, size_x, size_y, size_z, category, setup: bool = False):
|
||||
super().__init__(...)
|
||||
if setup:
|
||||
self.setup()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. `unilabos/resources/graphio.py`
|
||||
|
||||
- 修复 `resource_bioyond_to_plr` 中两处 `bottle.tracker.liquids` 直接赋值导致的崩溃
|
||||
- `ResourceHolder`(如枪头盒的 TipSpot 槽位)没有 `tracker` 属性,直接访问会抛出 `AttributeError`,阻断整个 Bioyond 同步流程
|
||||
|
||||
```python
|
||||
# 修复前
|
||||
bottle.tracker.liquids = [...]
|
||||
|
||||
# 修复后
|
||||
if hasattr(bottle, 'tracker') and bottle.tracker is not None:
|
||||
bottle.tracker.liquids = [...]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. `unilabos/app/main.py`
|
||||
|
||||
- 保留 `file_path is not None` 条件不变(已还原),并补充注释说明原因
|
||||
- 该逻辑只在**本地文件模式**下有意义:本地 graph 文件只含设备结构,远端有已保存物料,merge 才能将两者合并
|
||||
- 远端模式(`file_path=None`)下,`resource_tree_set` 和 `request_startup_json` 来自同一份数据,merge 为空操作,条件是否加 `file_path is not None` 对结果没有影响
|
||||
|
||||
---
|
||||
|
||||
### 4. `unilabos/devices/workstation/bioyond_studio/station.py` ⭐ 核心修复
|
||||
|
||||
- 当 deck 通过反序列化创建时,不会自动调用 `setup()`,导致 `deck.children` 为空,`warehouses` 始终是 `{}`
|
||||
- 增加兜底逻辑:仓库扫描后仍为空,则主动调用 `deck.setup()` 初始化仓库
|
||||
- 这是导致所有物料放置失败(`warehouse '...' 在deck中不存在。可用warehouses: []`)的根本原因
|
||||
|
||||
```python
|
||||
# 新增兜底
|
||||
if not self.deck.warehouses and hasattr(self.deck, "setup") and callable(self.deck.setup):
|
||||
logger.info("Deck 无仓库子节点,调用 setup() 初始化仓库")
|
||||
self.deck.setup()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 补充修复 2026-03-25:依华扣电组装工站子物料未上传
|
||||
|
||||
### 问题
|
||||
|
||||
`CoinCellAssemblyWorkstation.post_init` 直接上传空 deck,未调用 `deck.setup()`,导致:
|
||||
- 前端子物料(成品弹夹、料盘、瓶架等)不显示
|
||||
- 运行时 `self.deck.get_resource("成品弹夹")` 抛出 `ResourceNotFoundError`
|
||||
|
||||
### 修复文件
|
||||
|
||||
**`unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py`**
|
||||
- `YihuaCoinCellDeck.__init__` 补回 `setup: bool = False` 参数及 `if setup: self.setup()` 逻辑
|
||||
|
||||
**`unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py`**
|
||||
- `post_init` 中增加与 Bioyond 工站相同的兜底逻辑:deck 无子节点时调用 `deck.setup()` 初始化
|
||||
|
||||
```python
|
||||
# post_init 中新增
|
||||
if self.deck and not self.deck.children and hasattr(self.deck, "setup") and callable(self.deck.setup):
|
||||
logger.info("YihuaCoinCellDeck 无子节点,调用 setup() 初始化")
|
||||
self.deck.setup()
|
||||
```
|
||||
|
||||
### 联动 Bug:`MaterialPlate.create_with_holes` 构造顺序错误
|
||||
|
||||
**现象**:`deck.setup()` 被调用后,启动时抛出:
|
||||
```
|
||||
设备后初始化失败: Must specify either `ordered_items` or `ordering`.
|
||||
```
|
||||
|
||||
**根因**:`create_with_holes` 原来的逻辑是先构造空的 `MaterialPlate` 实例,再 assign 洞位:
|
||||
```python
|
||||
# 旧(错误):cls(...) 时 ordered_items=None → ItemizedResource.__init__ 立即报错
|
||||
plate = cls(name=name, ...) # ← 这里就崩了
|
||||
holes = create_ordered_items_2d(...) # ← 根本没走到这里
|
||||
for hole_name, hole in holes.items():
|
||||
plate.assign_child_resource(...)
|
||||
```
|
||||
pylabrobot 的 `ItemizedResource.__init__` 强制要求 `ordered_items` 和 `ordering` 必须有一个不为 `None`,空构造直接失败。
|
||||
|
||||
**修复**:先建洞位,再作为 `ordered_items` 传给构造函数:
|
||||
```python
|
||||
# 新(正确):先建洞位,再一次性传入构造函数
|
||||
holes = create_ordered_items_2d(klass=MaterialHole, num_items_x=4, ...)
|
||||
return cls(name=name, ..., ordered_items=holes)
|
||||
```
|
||||
|
||||
> 此 bug 此前未被触发,是因为 `deck.setup()` 从未被调用到——正是上面 `post_init` 兜底修复引出的联动问题。
|
||||
|
||||
---
|
||||
|
||||
## 补充修复 2026-03-25:3→2→1 转运资源同步失败
|
||||
|
||||
### 问题
|
||||
|
||||
配液工站(Bioyond)完成分液后,调用 `transfer_3_to_2_to_1_auto` 将分液瓶板转运到扣电工站(BatteryStation)。物理 LIMS 转运成功,但数字孪生资源树同步始终失败:
|
||||
```
|
||||
[资源同步] ❌ 失败: 目标设备 'BatteryStation' 中未找到资源 'bottle_rack_6x2'
|
||||
```
|
||||
|
||||
### 根因
|
||||
|
||||
`_get_resource_from_device` 方法负责跨设备查找资源对象,有两个问题:
|
||||
|
||||
1. **原始路径完全失效**:尝试 `from unilabos.app.ros2_app import get_device_plr_resource_by_name`,但该模块不存在,`ImportError` 被 `except Exception: pass` 静默吞掉
|
||||
2. **降级路径搜错地方**:遍历 `self._plr_resources`(Bioyond 自己的资源),不可能找到 BatteryStation 的 `bottle_rack_6x2`
|
||||
|
||||
### 修复文件
|
||||
|
||||
**`unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py`**
|
||||
|
||||
改用全局设备注册表 `registered_devices` 跨设备访问目标 deck:
|
||||
|
||||
```python
|
||||
# 修复前(失效)
|
||||
from unilabos.app.ros2_app import get_device_plr_resource_by_name # 模块不存在
|
||||
return get_device_plr_resource_by_name(device_id, resource_name)
|
||||
|
||||
# 修复后
|
||||
from unilabos.ros.nodes.base_device_node import registered_devices
|
||||
device_info = registered_devices.get(device_id)
|
||||
if device_info is not None:
|
||||
driver = device_info.get("driver_instance") # TypedDict 是 dict,必须用 .get()
|
||||
if driver is not None:
|
||||
deck = getattr(driver, "deck", None)
|
||||
if deck is not None:
|
||||
res = deck.get_resource(resource_name)
|
||||
```
|
||||
|
||||
关键细节:`DeviceInfoType` 是 `TypedDict`(即普通 `dict`),必须用 `device_info.get("driver_instance")` 而非 `getattr(device_info, "driver_instance", None)`——后者对字典永远返回 `None`。
|
||||
|
||||
---
|
||||
|
||||
## 根本原因分析
|
||||
|
||||
旧版以**本地文件模式**启动(有 `graph` 文件),deck 在启动前已通过 `merge_remote_resources` 获得仓库子节点,反序列化时能正确恢复 warehouses。
|
||||
|
||||
新版以**远端模式**启动(`file_path=None`),deck 反序列化时没有仓库子节点,`station.py` 扫描为空,所有物料的 warehouse 匹配失败,Bioyond 同步的 16 个资源全部无法放置到对应仓库位,前端不显示。
|
||||
@@ -12,7 +12,7 @@ Uni-Lab 使用 Python 格式的配置文件(`.py`),默认为 `unilabos_dat
|
||||
|
||||
**获取方式:**
|
||||
|
||||
进入 [Uni-Lab 实验室](https://uni-lab.bohrium.com),点击左下角的头像,在实验室详情中获取所在实验室的 ak 和 sk:
|
||||
进入 [Uni-Lab 实验室](https://leap-lab.bohrium.com),点击左下角的头像,在实验室详情中获取所在实验室的 ak 和 sk:
|
||||
|
||||

|
||||
|
||||
@@ -69,7 +69,7 @@ class WSConfig:
|
||||
|
||||
# HTTP配置
|
||||
class HTTPConfig:
|
||||
remote_addr = "https://uni-lab.bohrium.com/api/v1" # 远程服务器地址
|
||||
remote_addr = "https://leap-lab.bohrium.com/api/v1" # 远程服务器地址
|
||||
|
||||
# ROS配置
|
||||
class ROSConfig:
|
||||
@@ -209,8 +209,8 @@ unilab --ak "key" --sk "secret" --addr "test" --upload_registry --2d_vis -g grap
|
||||
|
||||
`--addr` 参数支持以下预设值,会自动转换为对应的完整 URL:
|
||||
|
||||
- `test` → `https://uni-lab.test.bohrium.com/api/v1`
|
||||
- `uat` → `https://uni-lab.uat.bohrium.com/api/v1`
|
||||
- `test` → `https://leap-lab.test.bohrium.com/api/v1`
|
||||
- `uat` → `https://leap-lab.uat.bohrium.com/api/v1`
|
||||
- `local` → `http://127.0.0.1:48197/api/v1`
|
||||
- 其他值 → 直接使用作为完整 URL
|
||||
|
||||
@@ -248,7 +248,7 @@ unilab --ak "key" --sk "secret" --addr "test" --upload_registry --2d_vis -g grap
|
||||
|
||||
`ak` 和 `sk` 是必需的认证参数:
|
||||
|
||||
1. **获取方式**:在 [Uni-Lab 官网](https://uni-lab.bohrium.com) 注册实验室后获得
|
||||
1. **获取方式**:在 [Uni-Lab 官网](https://leap-lab.bohrium.com) 注册实验室后获得
|
||||
2. **配置方式**:
|
||||
- **命令行参数**:`--ak "your_key" --sk "your_secret"`(最高优先级,推荐)
|
||||
- **环境变量**:`UNILABOS_BASICCONFIG_AK` 和 `UNILABOS_BASICCONFIG_SK`
|
||||
@@ -275,15 +275,15 @@ WebSocket 是 Uni-Lab 的主要通信方式:
|
||||
|
||||
HTTP 客户端配置用于与云端服务通信:
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
| ------------- | ---- | -------------------------------------- | ------------ |
|
||||
| `remote_addr` | str | `"https://uni-lab.bohrium.com/api/v1"` | 远程服务地址 |
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
| ------------- | ---- | --------------------------------------- | ------------ |
|
||||
| `remote_addr` | str | `"https://leap-lab.bohrium.com/api/v1"` | 远程服务地址 |
|
||||
|
||||
**预设环境地址**:
|
||||
|
||||
- 生产环境:`https://uni-lab.bohrium.com/api/v1`(默认)
|
||||
- 测试环境:`https://uni-lab.test.bohrium.com/api/v1`
|
||||
- UAT 环境:`https://uni-lab.uat.bohrium.com/api/v1`
|
||||
- 生产环境:`https://leap-lab.bohrium.com/api/v1`(默认)
|
||||
- 测试环境:`https://leap-lab.test.bohrium.com/api/v1`
|
||||
- UAT 环境:`https://leap-lab.uat.bohrium.com/api/v1`
|
||||
- 本地环境:`http://127.0.0.1:48197/api/v1`
|
||||
|
||||
### 4. ROSConfig - ROS 配置
|
||||
@@ -401,7 +401,7 @@ export UNILABOS_WSCONFIG_RECONNECT_INTERVAL="10"
|
||||
export UNILABOS_WSCONFIG_MAX_RECONNECT_ATTEMPTS="500"
|
||||
|
||||
# 设置HTTP配置
|
||||
export UNILABOS_HTTPCONFIG_REMOTE_ADDR="https://uni-lab.test.bohrium.com/api/v1"
|
||||
export UNILABOS_HTTPCONFIG_REMOTE_ADDR="https://leap-lab.test.bohrium.com/api/v1"
|
||||
```
|
||||
|
||||
## 配置文件使用方法
|
||||
@@ -484,13 +484,13 @@ export UNILABOS_WSCONFIG_MAX_RECONNECT_ATTEMPTS=100
|
||||
|
||||
```python
|
||||
class HTTPConfig:
|
||||
remote_addr = "https://uni-lab.test.bohrium.com/api/v1"
|
||||
remote_addr = "https://leap-lab.test.bohrium.com/api/v1"
|
||||
```
|
||||
|
||||
**环境变量方式:**
|
||||
|
||||
```bash
|
||||
export UNILABOS_HTTPCONFIG_REMOTE_ADDR=https://uni-lab.test.bohrium.com/api/v1
|
||||
export UNILABOS_HTTPCONFIG_REMOTE_ADDR=https://leap-lab.test.bohrium.com/api/v1
|
||||
```
|
||||
|
||||
**命令行方式(推荐):**
|
||||
|
||||
@@ -15,6 +15,9 @@ Python 类设备驱动在完成注册表后可以直接在 Uni-Lab 中使用,
|
||||
**示例:**
|
||||
|
||||
```python
|
||||
from unilabos.registry.decorators import device, topic_config
|
||||
|
||||
@device(id="mock_gripper", category=["gripper"], description="Mock Gripper")
|
||||
class MockGripper:
|
||||
def __init__(self):
|
||||
self._position: float = 0.0
|
||||
@@ -23,19 +26,23 @@ class MockGripper:
|
||||
self._status = "Idle"
|
||||
|
||||
@property
|
||||
@topic_config() # 添加 @topic_config 才会定时广播
|
||||
def position(self) -> float:
|
||||
return self._position
|
||||
|
||||
@property
|
||||
@topic_config()
|
||||
def velocity(self) -> float:
|
||||
return self._velocity
|
||||
|
||||
@property
|
||||
@topic_config()
|
||||
def torque(self) -> float:
|
||||
return self._torque
|
||||
|
||||
# 会被自动识别的设备属性,接入 Uni-Lab 时会定时对外广播
|
||||
# 使用 @topic_config 装饰的属性,接入 Uni-Lab 时会定时对外广播
|
||||
@property
|
||||
@topic_config(period=2.0) # 可自定义发布周期
|
||||
def status(self) -> str:
|
||||
return self._status
|
||||
|
||||
@@ -149,7 +156,7 @@ my_device: # 设备唯一标识符
|
||||
|
||||
系统会自动分析您的 Python 驱动类并生成:
|
||||
|
||||
- `status_types`:从 `@property` 装饰的方法自动识别状态属性
|
||||
- `status_types`:从 `@topic_config` 装饰的 `@property` 或方法自动识别状态属性
|
||||
- `action_value_mappings`:从类方法自动生成动作映射
|
||||
- `init_param_schema`:从 `__init__` 方法分析初始化参数
|
||||
- `schema`:前端显示用的属性类型定义
|
||||
@@ -179,7 +186,9 @@ Uni-Lab 设备驱动是一个 Python 类,需要遵循以下结构:
|
||||
|
||||
```python
|
||||
from typing import Dict, Any
|
||||
from unilabos.registry.decorators import device, topic_config
|
||||
|
||||
@device(id="my_device", category=["general"], description="My Device")
|
||||
class MyDevice:
|
||||
"""设备类文档字符串
|
||||
|
||||
@@ -198,8 +207,9 @@ class MyDevice:
|
||||
# 初始化硬件连接
|
||||
|
||||
@property
|
||||
@topic_config() # 必须添加 @topic_config 才会广播
|
||||
def status(self) -> str:
|
||||
"""设备状态(会自动广播)"""
|
||||
"""设备状态(通过 @topic_config 广播)"""
|
||||
return self._status
|
||||
|
||||
def my_action(self, param: float) -> Dict[str, Any]:
|
||||
@@ -217,34 +227,61 @@ class MyDevice:
|
||||
|
||||
## 状态属性 vs 动作方法
|
||||
|
||||
### 状态属性(@property)
|
||||
### 状态属性(@property + @topic_config)
|
||||
|
||||
状态属性会被自动识别并定期广播:
|
||||
状态属性需要同时使用 `@property` 和 `@topic_config` 装饰器才会被识别并定期广播:
|
||||
|
||||
```python
|
||||
from unilabos.registry.decorators import topic_config
|
||||
|
||||
@property
|
||||
@topic_config() # 必须添加,否则不会广播
|
||||
def temperature(self) -> float:
|
||||
"""当前温度"""
|
||||
return self._read_temperature()
|
||||
|
||||
@property
|
||||
@topic_config(period=2.0) # 可自定义发布周期(秒)
|
||||
def status(self) -> str:
|
||||
"""设备状态: idle, running, error"""
|
||||
return self._status
|
||||
|
||||
@property
|
||||
@topic_config(name="ready") # 可自定义发布名称
|
||||
def is_ready(self) -> bool:
|
||||
"""设备是否就绪"""
|
||||
return self._status == "idle"
|
||||
```
|
||||
|
||||
也可以使用普通方法(非 @property)配合 `@topic_config`:
|
||||
|
||||
```python
|
||||
@topic_config(period=10.0)
|
||||
def get_sensor_data(self) -> Dict[str, float]:
|
||||
"""获取传感器数据(get_ 前缀会自动去除,发布名为 sensor_data)"""
|
||||
return {"temp": self._temp, "humidity": self._humidity}
|
||||
```
|
||||
|
||||
**`@topic_config` 参数**:
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `period` | float | 5.0 | 发布周期(秒) |
|
||||
| `print_publish` | bool | 节点默认 | 是否打印发布日志 |
|
||||
| `qos` | int | 10 | QoS 深度 |
|
||||
| `name` | str | None | 自定义发布名称 |
|
||||
|
||||
**发布名称优先级**:`@topic_config(name=...)` > `get_` 前缀去除 > 方法名
|
||||
|
||||
**特点**:
|
||||
|
||||
- 使用`@property`装饰器
|
||||
- 只读,不能有参数
|
||||
- 自动添加到注册表的`status_types`
|
||||
- 必须使用 `@topic_config` 装饰器
|
||||
- 支持 `@property` 和普通方法
|
||||
- 添加到注册表的 `status_types`
|
||||
- 定期发布到 ROS2 topic
|
||||
|
||||
> **⚠️ 重要:** 仅有 `@property` 装饰器而没有 `@topic_config` 的属性**不会**被广播。这是一个 Breaking Change。
|
||||
|
||||
### 动作方法
|
||||
|
||||
动作方法是设备可以执行的操作:
|
||||
@@ -497,6 +534,7 @@ class LiquidHandler:
|
||||
self._status = "idle"
|
||||
|
||||
@property
|
||||
@topic_config()
|
||||
def status(self) -> str:
|
||||
return self._status
|
||||
|
||||
@@ -886,7 +924,52 @@ class MyDevice:
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 类型注解
|
||||
### 1. 使用 `@device` 装饰器标识设备类
|
||||
|
||||
```python
|
||||
from unilabos.registry.decorators import device
|
||||
|
||||
@device(id="my_device", category=["heating"], description="My Heating Device", icon="heater.webp")
|
||||
class MyDevice:
|
||||
...
|
||||
```
|
||||
|
||||
- `id`:设备唯一标识符,用于注册表匹配
|
||||
- `category`:分类列表,前端用于分组显示
|
||||
- `description`:设备描述
|
||||
- `icon`:图标文件名(可选)
|
||||
|
||||
### 2. 使用 `@topic_config` 声明需要广播的状态
|
||||
|
||||
```python
|
||||
from unilabos.registry.decorators import topic_config
|
||||
|
||||
# ✓ @property + @topic_config → 会广播
|
||||
@property
|
||||
@topic_config(period=2.0)
|
||||
def temperature(self) -> float:
|
||||
return self._temp
|
||||
|
||||
# ✓ 普通方法 + @topic_config → 会广播(get_ 前缀自动去除)
|
||||
@topic_config(period=10.0)
|
||||
def get_sensor_data(self) -> Dict[str, float]:
|
||||
return {"temp": self._temp}
|
||||
|
||||
# ✓ 使用 name 参数自定义发布名称
|
||||
@property
|
||||
@topic_config(name="ready")
|
||||
def is_ready(self) -> bool:
|
||||
return self._status == "idle"
|
||||
|
||||
# ✗ 仅有 @property,没有 @topic_config → 不会广播
|
||||
@property
|
||||
def internal_state(self) -> str:
|
||||
return self._state
|
||||
```
|
||||
|
||||
> **注意:** 与 `@property` 连用时,`@topic_config` 必须放在 `@property` 下面。
|
||||
|
||||
### 3. 类型注解
|
||||
|
||||
```python
|
||||
from typing import Dict, Any, Optional, List
|
||||
@@ -901,7 +984,7 @@ def method(
|
||||
pass
|
||||
```
|
||||
|
||||
### 2. 文档字符串
|
||||
### 4. 文档字符串
|
||||
|
||||
```python
|
||||
def method(self, param: float) -> Dict[str, Any]:
|
||||
@@ -923,7 +1006,7 @@ def method(self, param: float) -> Dict[str, Any]:
|
||||
pass
|
||||
```
|
||||
|
||||
### 3. 配置验证
|
||||
### 5. 配置验证
|
||||
|
||||
```python
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
@@ -937,7 +1020,7 @@ def __init__(self, config: Dict[str, Any]):
|
||||
self.baudrate = config['baudrate']
|
||||
```
|
||||
|
||||
### 4. 资源清理
|
||||
### 6. 资源清理
|
||||
|
||||
```python
|
||||
def __del__(self):
|
||||
@@ -946,7 +1029,7 @@ def __del__(self):
|
||||
self.connection.close()
|
||||
```
|
||||
|
||||
### 5. 设计前端友好的返回值
|
||||
### 7. 设计前端友好的返回值
|
||||
|
||||
**记住:返回值会直接显示在 Web 界面**
|
||||
|
||||
|
||||
@@ -422,18 +422,20 @@ placeholder_keys:
|
||||
|
||||
### status_types
|
||||
|
||||
系统会扫描你的 Python 类,从状态方法(property 或 get\_方法)自动生成这部分:
|
||||
系统会扫描你的 Python 类,从带有 `@topic_config` 装饰器的 `@property` 或方法自动生成这部分:
|
||||
|
||||
```yaml
|
||||
status_types:
|
||||
current_temperature: float # 从 get_current_temperature() 或 @property current_temperature
|
||||
is_heating: bool # 从 get_is_heating() 或 @property is_heating
|
||||
status: str # 从 get_status() 或 @property status
|
||||
current_temperature: float # 从 @topic_config 装饰的 @property 或方法
|
||||
is_heating: bool
|
||||
status: str
|
||||
```
|
||||
|
||||
**注意事项**:
|
||||
|
||||
- 系统会查找所有 `get_` 开头的方法和 `@property` 装饰的属性
|
||||
- 仅有带 `@topic_config` 装饰器的 `@property` 或方法才会被识别为状态属性
|
||||
- 没有 `@topic_config` 的 `@property` 不会生成 status_types,也不会广播
|
||||
- `get_` 前缀的方法名会自动去除前缀(如 `get_temperature` → `temperature`)
|
||||
- 类型会自动转成相应的类型(如 `str`、`float`、`bool`)
|
||||
- 如果类型是 `Any`、`None` 或未知的,默认使用 `String`
|
||||
|
||||
@@ -537,11 +539,13 @@ class AdvancedLiquidHandler:
|
||||
self._temperature = 25.0
|
||||
|
||||
@property
|
||||
@topic_config()
|
||||
def status(self) -> str:
|
||||
"""设备状态"""
|
||||
return self._status
|
||||
|
||||
@property
|
||||
@topic_config()
|
||||
def temperature(self) -> float:
|
||||
"""当前温度"""
|
||||
return self._temperature
|
||||
@@ -809,21 +813,23 @@ my_temperature_controller:
|
||||
你的设备类需要符合以下要求:
|
||||
|
||||
```python
|
||||
from unilabos.common.device_base import DeviceBase
|
||||
from unilabos.registry.decorators import device, topic_config
|
||||
|
||||
class MyDevice(DeviceBase):
|
||||
@device(id="my_device", category=["temperature"], description="My Device")
|
||||
class MyDevice:
|
||||
def __init__(self, config):
|
||||
"""初始化,参数会自动分析到 init_param_schema.config"""
|
||||
super().__init__(config)
|
||||
self.port = config.get('port', '/dev/ttyUSB0')
|
||||
|
||||
# 状态方法(会自动生成到 status_types)
|
||||
# 状态方法(必须添加 @topic_config 才会生成到 status_types 并广播)
|
||||
@property
|
||||
@topic_config()
|
||||
def status(self):
|
||||
"""返回设备状态"""
|
||||
return "idle"
|
||||
|
||||
@property
|
||||
@topic_config()
|
||||
def temperature(self):
|
||||
"""返回当前温度"""
|
||||
return 25.0
|
||||
@@ -1039,7 +1045,34 @@ resource.type # "resource"
|
||||
|
||||
### 代码规范
|
||||
|
||||
1. **始终使用类型注解**
|
||||
1. **使用 `@device` 装饰器标识设备类**
|
||||
|
||||
```python
|
||||
from unilabos.registry.decorators import device
|
||||
|
||||
@device(id="my_device", category=["heating"], description="My Device")
|
||||
class MyDevice:
|
||||
...
|
||||
```
|
||||
|
||||
2. **使用 `@topic_config` 声明广播属性**
|
||||
|
||||
```python
|
||||
from unilabos.registry.decorators import topic_config
|
||||
|
||||
# ✓ 需要广播的状态属性
|
||||
@property
|
||||
@topic_config(period=2.0)
|
||||
def temperature(self) -> float:
|
||||
return self._temp
|
||||
|
||||
# ✗ 仅有 @property 不会广播
|
||||
@property
|
||||
def internal_counter(self) -> int:
|
||||
return self._counter
|
||||
```
|
||||
|
||||
3. **始终使用类型注解**
|
||||
|
||||
```python
|
||||
# ✓ 好
|
||||
@@ -1051,7 +1084,7 @@ def method(self, resource, device):
|
||||
pass
|
||||
```
|
||||
|
||||
2. **提供有意义的参数名**
|
||||
4. **提供有意义的参数名**
|
||||
|
||||
```python
|
||||
# ✓ 好 - 清晰的参数名
|
||||
@@ -1063,7 +1096,7 @@ def transfer(self, r1: ResourceSlot, r2: ResourceSlot):
|
||||
pass
|
||||
```
|
||||
|
||||
3. **使用 Optional 表示可选参数**
|
||||
5. **使用 Optional 表示可选参数**
|
||||
|
||||
```python
|
||||
from typing import Optional
|
||||
@@ -1076,7 +1109,7 @@ def method(
|
||||
pass
|
||||
```
|
||||
|
||||
4. **添加详细的文档字符串**
|
||||
6. **添加详细的文档字符串**
|
||||
|
||||
```python
|
||||
def method(
|
||||
@@ -1096,13 +1129,13 @@ def method(
|
||||
pass
|
||||
```
|
||||
|
||||
5. **方法命名规范**
|
||||
7. **方法命名规范**
|
||||
|
||||
- 状态方法使用 `@property` 装饰器或 `get_` 前缀
|
||||
- 状态方法使用 `@property` + `@topic_config` 装饰器,或普通方法 + `@topic_config`
|
||||
- 动作方法使用动词开头
|
||||
- 保持命名清晰、一致
|
||||
|
||||
6. **完善的错误处理**
|
||||
8. **完善的错误处理**
|
||||
- 实现完善的错误处理
|
||||
- 添加日志记录
|
||||
- 提供有意义的错误信息
|
||||
|
||||
@@ -23,7 +23,7 @@ Uni-Lab-OS 支持多种部署模式:
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ Cloud Platform/Self-hosted Platform │
|
||||
│ uni-lab.bohrium.com │
|
||||
│ leap-lab.bohrium.com │
|
||||
│ (Resource Management, Task Scheduling, │
|
||||
│ Monitoring) │
|
||||
└────────────────────┬─────────────────────────┘
|
||||
@@ -221,10 +221,10 @@ Laboratory A Laboratory B
|
||||
|
||||
```bash
|
||||
# 实验室A
|
||||
unilab --ak your_ak --sk your_sk --upload_registry --use_remote_resource
|
||||
unilab --ak your_ak --sk your_sk --upload_registry
|
||||
|
||||
# 实验室B
|
||||
unilab --ak your_ak --sk your_sk --upload_registry --use_remote_resource
|
||||
unilab --ak your_ak --sk your_sk --upload_registry
|
||||
```
|
||||
|
||||
---
|
||||
@@ -444,7 +444,7 @@ ros2 daemon stop && ros2 daemon start
|
||||
|
||||
```bash
|
||||
# 测试云端连接
|
||||
curl https://uni-lab.bohrium.com/api/v1/health
|
||||
curl https://leap-lab.bohrium.com/api/v1/health
|
||||
|
||||
# 测试WebSocket
|
||||
# 启动Uni-Lab后查看日志
|
||||
|
||||
@@ -33,11 +33,11 @@
|
||||
|
||||
**选择合适的安装包:**
|
||||
|
||||
| 安装包 | 适用场景 | 包含组件 |
|
||||
|--------|----------|----------|
|
||||
| `unilabos` | **推荐大多数用户**,生产部署 | 完整安装包,开箱即用 |
|
||||
| `unilabos-env` | 开发者(可编辑安装) | 仅环境依赖,通过 pip 安装 unilabos |
|
||||
| `unilabos-full` | 仿真/可视化 | unilabos + 完整 ROS2 桌面版 + Gazebo + MoveIt |
|
||||
| 安装包 | 适用场景 | 包含组件 |
|
||||
| --------------- | ---------------------------- | --------------------------------------------- |
|
||||
| `unilabos` | **推荐大多数用户**,生产部署 | 完整安装包,开箱即用 |
|
||||
| `unilabos-env` | 开发者(可编辑安装) | 仅环境依赖,通过 pip 安装 unilabos |
|
||||
| `unilabos-full` | 仿真/可视化 | unilabos + 完整 ROS2 桌面版 + Gazebo + MoveIt |
|
||||
|
||||
**关键步骤:**
|
||||
|
||||
@@ -66,6 +66,7 @@ mamba install uni-lab::unilabos-full -c robostack-staging -c conda-forge
|
||||
```
|
||||
|
||||
**选择建议:**
|
||||
|
||||
- **日常使用/生产部署**:使用 `unilabos`(推荐),完整功能,开箱即用
|
||||
- **开发者**:使用 `unilabos-env` + `pip install -e .` + `uv pip install -r unilabos/utils/requirements.txt`,代码修改立即生效
|
||||
- **仿真/可视化**:使用 `unilabos-full`,含 Gazebo、rviz2、MoveIt
|
||||
@@ -88,7 +89,7 @@ python -c "from unilabos_msgs.msg import Resource; print('ROS msgs OK')"
|
||||
|
||||
#### 2.1 注册实验室账号
|
||||
|
||||
1. 访问 [https://uni-lab.bohrium.com](https://uni-lab.bohrium.com)
|
||||
1. 访问 [https://leap-lab.bohrium.com](https://leap-lab.bohrium.com)
|
||||
2. 注册账号并登录
|
||||
3. 创建新实验室
|
||||
|
||||
@@ -297,7 +298,7 @@ unilab --ak your_ak --sk your_sk -g test/experiments/mock_devices/mock_all.json
|
||||
|
||||
#### 5.2 访问 Web 界面
|
||||
|
||||
启动系统后,访问[https://uni-lab.bohrium.com](https://uni-lab.bohrium.com)
|
||||
启动系统后,访问[https://leap-lab.bohrium.com](https://leap-lab.bohrium.com)
|
||||
|
||||
#### 5.3 添加设备和物料
|
||||
|
||||
@@ -306,12 +307,10 @@ unilab --ak your_ak --sk your_sk -g test/experiments/mock_devices/mock_all.json
|
||||
**示例场景:** 创建一个简单的液体转移实验
|
||||
|
||||
1. **添加工作站(必需):**
|
||||
|
||||
- 在"仪器设备"中找到 `work_station`
|
||||
- 添加 `workstation` x1
|
||||
|
||||
2. **添加虚拟转移泵:**
|
||||
|
||||
- 在"仪器设备"中找到 `virtual_device`
|
||||
- 添加 `virtual_transfer_pump` x1
|
||||
|
||||
@@ -452,8 +451,9 @@ unilab --ak your_ak --sk your_sk -g test/experiments/mock_devices/mock_all.json
|
||||
**操作步骤:**
|
||||
|
||||
1. 将两个 `container` 拖拽到 `workstation` 中
|
||||
2. 将 `virtual_transfer_pump` 拖拽到 `workstation` 中
|
||||
3. 在画布上连接它们(建立父子关系)
|
||||
2. 将 `virtual_multiway_valve` 拖拽到 `workstation` 中
|
||||
3. 将 `virtual_transfer_pump` 拖拽到 `workstation` 中
|
||||
4. 在画布上连接它们(建立父子关系)
|
||||
|
||||

|
||||
|
||||
@@ -817,6 +817,7 @@ uv pip install -r unilabos/utils/requirements.txt
|
||||
```
|
||||
|
||||
**为什么使用这种方式?**
|
||||
|
||||
- `unilabos-env` 提供 ROS2 核心组件和 uv(通过 conda 安装,避免编译)
|
||||
- `unilabos/utils/requirements.txt` 包含所有运行时需要的 pip 依赖
|
||||
- `dev_install.py` 自动检测中文环境,中文系统自动使用清华镜像
|
||||
@@ -1795,32 +1796,27 @@ unilab --ak your_ak --sk your_sk -g graph.json \
|
||||
**详细步骤:**
|
||||
|
||||
1. **需求分析**:
|
||||
|
||||
- 明确实验流程
|
||||
- 列出所需设备和物料
|
||||
- 设计工作流程图
|
||||
|
||||
2. **环境搭建**:
|
||||
|
||||
- 安装 Uni-Lab-OS
|
||||
- 创建实验室账号
|
||||
- 准备开发工具(IDE、Git)
|
||||
|
||||
3. **原型验证**:
|
||||
|
||||
- 使用虚拟设备测试流程
|
||||
- 验证工作流逻辑
|
||||
- 调整参数
|
||||
|
||||
4. **迭代开发**:
|
||||
|
||||
- 实现自定义设备驱动(同时撰写单点函数测试)
|
||||
- 编写注册表
|
||||
- 单元测试
|
||||
- 集成测试
|
||||
|
||||
5. **测试部署**:
|
||||
|
||||
- 连接真实硬件
|
||||
- 空跑测试
|
||||
- 小规模试验
|
||||
@@ -1870,7 +1866,7 @@ unilab --ak your_ak --sk your_sk -g graph.json \
|
||||
#### 14.5 社区支持
|
||||
|
||||
- **GitHub Issues**:[https://github.com/deepmodeling/Uni-Lab-OS/issues](https://github.com/deepmodeling/Uni-Lab-OS/issues)
|
||||
- **官方网站**:[https://uni-lab.bohrium.com](https://uni-lab.bohrium.com)
|
||||
- **官方网站**:[https://leap-lab.bohrium.com](https://leap-lab.bohrium.com)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -626,7 +626,7 @@ unilab
|
||||
|
||||
**云端图文件管理**:
|
||||
|
||||
1. 登录 https://uni-lab.bohrium.com
|
||||
1. 登录 https://leap-lab.bohrium.com
|
||||
2. 进入"设备配置"
|
||||
3. 创建或编辑配置
|
||||
4. 保存到云端
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 275 KiB After Width: | Height: | Size: 415 KiB |
@@ -22,7 +22,6 @@ options:
|
||||
--is_slave Run the backend as slave node (without host privileges).
|
||||
--slave_no_host Skip waiting for host service in slave mode
|
||||
--upload_registry Upload registry information when starting unilab
|
||||
--use_remote_resource Use remote resources when starting unilab
|
||||
--config CONFIG Configuration file path, supports .py format Python config files
|
||||
--port PORT Port for web service information page
|
||||
--disable_browser Disable opening information page on startup
|
||||
@@ -55,7 +54,6 @@ Uni-Lab 的启动过程分为以下几个阶段:
|
||||
您可以直接跟随 unilabos 的提示进行,无需查阅本节
|
||||
|
||||
- **工作目录设置**:
|
||||
|
||||
- 如果当前目录以 `unilabos_data` 结尾,则使用当前目录
|
||||
- 否则使用 `当前目录/unilabos_data` 作为工作目录
|
||||
- 可通过 `--working_dir` 指定自定义工作目录
|
||||
@@ -69,8 +67,8 @@ Uni-Lab 的启动过程分为以下几个阶段:
|
||||
|
||||
支持多种后端环境:
|
||||
|
||||
- `--addr test`:测试环境 (`https://uni-lab.test.bohrium.com/api/v1`)
|
||||
- `--addr uat`:UAT 环境 (`https://uni-lab.uat.bohrium.com/api/v1`)
|
||||
- `--addr test`:测试环境 (`https://leap-lab.test.bohrium.com/api/v1`)
|
||||
- `--addr uat`:UAT 环境 (`https://leap-lab.uat.bohrium.com/api/v1`)
|
||||
- `--addr local`:本地环境 (`http://127.0.0.1:48197/api/v1`)
|
||||
- 自定义地址:直接指定完整 URL
|
||||
|
||||
@@ -85,7 +83,7 @@ Uni-Lab 的启动过程分为以下几个阶段:
|
||||
支持两种方式:
|
||||
|
||||
- **本地文件**:使用 `-g` 指定图谱文件(支持 JSON 和 GraphML 格式)
|
||||
- **远程资源**:使用 `--use_remote_resource` 从云端获取
|
||||
- **远程资源**:不指定本地文件即可
|
||||
|
||||
### 7. 注册表构建
|
||||
|
||||
@@ -177,7 +175,7 @@ unilab --config path/to/your/config.py
|
||||
|
||||
如果是首次使用,系统会:
|
||||
|
||||
1. 提示前往 https://uni-lab.bohrium.com 注册实验室
|
||||
1. 提示前往 https://leap-lab.bohrium.com 注册实验室
|
||||
2. 引导创建配置文件
|
||||
3. 设置工作目录
|
||||
|
||||
@@ -196,7 +194,7 @@ unilab --config path/to/your/config.py
|
||||
unilab --ak your_ak --sk your_sk -g path/to/graph.json --upload_registry
|
||||
|
||||
# 使用远程资源启动
|
||||
unilab --ak your_ak --sk your_sk --use_remote_resource
|
||||
unilab --ak your_ak --sk your_sk
|
||||
|
||||
# 更新注册表
|
||||
unilab --ak your_ak --sk your_sk --complete_registry
|
||||
@@ -217,7 +215,7 @@ unilab --ak your_ak --sk your_sk --port 8080 --disable_browser
|
||||
|
||||
如果提示 "后续运行必须拥有一个实验室",请确保:
|
||||
|
||||
- 已在 https://uni-lab.bohrium.com 注册实验室
|
||||
- 已在 https://leap-lab.bohrium.com 注册实验室
|
||||
- 正确设置了 `--ak` 和 `--sk` 参数
|
||||
- 配置文件中包含正确的认证信息
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
channel_sources:
|
||||
- robostack,robostack-staging,conda-forge,defaults
|
||||
- robostack,robostack-staging,conda-forge
|
||||
|
||||
gazebo:
|
||||
- '11'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package:
|
||||
name: ros-humble-unilabos-msgs
|
||||
version: 0.10.17
|
||||
version: 0.11.2
|
||||
source:
|
||||
path: ../../unilabos_msgs
|
||||
target_directory: src
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package:
|
||||
name: unilabos
|
||||
version: "0.10.17"
|
||||
version: "0.11.2"
|
||||
|
||||
source:
|
||||
path: ../..
|
||||
|
||||
2
setup.py
2
setup.py
@@ -4,7 +4,7 @@ package_name = 'unilabos'
|
||||
|
||||
setup(
|
||||
name=package_name,
|
||||
version='0.10.17',
|
||||
version='0.11.2',
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=['setuptools'],
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.10.17"
|
||||
__version__ = "0.11.2"
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
@@ -10,6 +12,15 @@ from typing import Dict, Any, List
|
||||
import networkx as nx
|
||||
import yaml
|
||||
|
||||
# Windows 中文系统 stdout 默认 GBK,无法编码 banner / emoji 日志中的 Unicode 字符
|
||||
# 强制 stdout/stderr 用 UTF-8,避免 print 触发 UnicodeEncodeError 导致进程崩溃
|
||||
if sys.platform == "win32":
|
||||
for _stream in (sys.stdout, sys.stderr):
|
||||
try:
|
||||
_stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[attr-defined]
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
|
||||
# 首先添加项目根目录到路径
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
unilabos_dir = os.path.dirname(os.path.dirname(current_dir))
|
||||
@@ -24,6 +35,84 @@ from unilabos.config.config import load_config, BasicConfig, HTTPConfig
|
||||
_restart_requested: bool = False
|
||||
_restart_reason: str = ""
|
||||
|
||||
RESTART_EXIT_CODE = 42
|
||||
|
||||
|
||||
def _build_child_argv():
|
||||
"""Build sys.argv for child process, stripping supervisor-only arguments."""
|
||||
result = []
|
||||
skip_next = False
|
||||
for arg in sys.argv:
|
||||
if skip_next:
|
||||
skip_next = False
|
||||
continue
|
||||
if arg in ("--restart_mode", "--restart-mode"):
|
||||
continue
|
||||
if arg in ("--auto_restart_count", "--auto-restart-count"):
|
||||
skip_next = True
|
||||
continue
|
||||
if arg.startswith("--auto_restart_count=") or arg.startswith("--auto-restart-count="):
|
||||
continue
|
||||
result.append(arg)
|
||||
return result
|
||||
|
||||
|
||||
def _run_as_supervisor(max_restarts: int):
|
||||
"""
|
||||
Supervisor process that spawns and monitors child processes.
|
||||
|
||||
Similar to Uvicorn's --reload: the supervisor itself does no heavy work,
|
||||
it only launches the real process as a child and restarts it when the child
|
||||
exits with RESTART_EXIT_CODE.
|
||||
"""
|
||||
child_argv = [sys.executable] + _build_child_argv()
|
||||
restart_count = 0
|
||||
|
||||
print_status(
|
||||
f"[Supervisor] Restart mode enabled (max restarts: {max_restarts}), "
|
||||
f"child command: {' '.join(child_argv)}",
|
||||
"info",
|
||||
)
|
||||
|
||||
while True:
|
||||
print_status(
|
||||
f"[Supervisor] Launching process (restart {restart_count}/{max_restarts})...",
|
||||
"info",
|
||||
)
|
||||
|
||||
try:
|
||||
process = subprocess.Popen(child_argv)
|
||||
exit_code = process.wait()
|
||||
except KeyboardInterrupt:
|
||||
print_status("[Supervisor] Interrupted, terminating child process...", "info")
|
||||
process.terminate()
|
||||
try:
|
||||
process.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
process.wait()
|
||||
sys.exit(1)
|
||||
|
||||
if exit_code == RESTART_EXIT_CODE:
|
||||
restart_count += 1
|
||||
if restart_count > max_restarts:
|
||||
print_status(
|
||||
f"[Supervisor] Maximum restart count ({max_restarts}) reached, exiting",
|
||||
"warning",
|
||||
)
|
||||
sys.exit(1)
|
||||
print_status(
|
||||
f"[Supervisor] Child requested restart ({restart_count}/{max_restarts}), restarting in 2s...",
|
||||
"info",
|
||||
)
|
||||
time.sleep(2)
|
||||
else:
|
||||
if exit_code != 0:
|
||||
print_status(f"[Supervisor] Child exited with code {exit_code}", "warning")
|
||||
else:
|
||||
print_status("[Supervisor] Child exited normally", "info")
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
def load_config_from_file(config_path):
|
||||
if config_path is None:
|
||||
@@ -65,6 +154,13 @@ def parse_args():
|
||||
action="append",
|
||||
help="Path to the registry directory",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--devices",
|
||||
type=str,
|
||||
default=None,
|
||||
action="append",
|
||||
help="Path to Python code directory for AST-based device/resource scanning",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--working_dir",
|
||||
type=str,
|
||||
@@ -146,7 +242,7 @@ def parse_args():
|
||||
parser.add_argument(
|
||||
"--addr",
|
||||
type=str,
|
||||
default="https://uni-lab.bohrium.com/api/v1",
|
||||
default="https://leap-lab.bohrium.com/api/v1",
|
||||
help="Laboratory backend address",
|
||||
)
|
||||
parser.add_argument(
|
||||
@@ -154,23 +250,53 @@ def parse_args():
|
||||
action="store_true",
|
||||
help="Skip environment dependency check on startup",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--complete_registry",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Complete registry information",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--check_mode",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Run in check mode for CI: validates registry imports and ensures no file changes",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--complete_registry",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Complete and rewrite YAML registry files using AST analysis results",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no_update_feedback",
|
||||
action="store_true",
|
||||
help="Disable sending update feedback to server",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--test_mode",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Test mode: all actions simulate execution and return mock results without running real hardware",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--external_devices_only",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Only load external device packages (--devices), skip built-in unilabos/devices/ scanning and YAML device registry",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--extra_resource",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Load extra lab_ prefixed labware resources (529 auto-generated definitions from lab_resources.py)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--restart_mode",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Enable supervisor mode: automatically restart the process when triggered via WebSocket",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--auto_restart_count",
|
||||
type=int,
|
||||
default=500,
|
||||
help="Maximum number of automatic restarts in restart mode (default: 500)",
|
||||
)
|
||||
# workflow upload subcommand
|
||||
workflow_parser = subparsers.add_parser(
|
||||
"workflow_upload",
|
||||
@@ -204,6 +330,12 @@ def parse_args():
|
||||
default=False,
|
||||
help="Whether to publish the workflow (default: False)",
|
||||
)
|
||||
workflow_parser.add_argument(
|
||||
"--description",
|
||||
type=str,
|
||||
default="",
|
||||
help="Workflow description, used when publishing the workflow",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
@@ -215,68 +347,88 @@ def main():
|
||||
args = parser.parse_args()
|
||||
args_dict = vars(args)
|
||||
|
||||
# Supervisor mode: spawn child processes and monitor for restart
|
||||
if args_dict.get("restart_mode", False):
|
||||
_run_as_supervisor(args_dict.get("auto_restart_count", 5))
|
||||
return
|
||||
|
||||
# 环境检查 - 检查并自动安装必需的包 (可选)
|
||||
skip_env_check = args_dict.get("skip_env_check", False)
|
||||
check_mode = args_dict.get("check_mode", False)
|
||||
|
||||
if not skip_env_check:
|
||||
from unilabos.utils.environment_check import check_environment
|
||||
from unilabos.utils.environment_check import check_environment, check_device_package_requirements
|
||||
|
||||
if not check_environment(auto_install=True):
|
||||
print_status("环境检查失败,程序退出", "error")
|
||||
os._exit(1)
|
||||
|
||||
# 第一次设备包依赖检查:build_registry 之前,确保 import map 可用
|
||||
devices_dirs_for_req = args_dict.get("devices", None)
|
||||
if devices_dirs_for_req:
|
||||
if not check_device_package_requirements(devices_dirs_for_req):
|
||||
print_status("设备包依赖检查失败,程序退出", "error")
|
||||
os._exit(1)
|
||||
else:
|
||||
print_status("跳过环境依赖检查", "warning")
|
||||
|
||||
# 加载配置文件,优先加载config,然后从env读取
|
||||
config_path = args_dict.get("config")
|
||||
|
||||
if check_mode:
|
||||
args_dict["working_dir"] = os.path.abspath(os.getcwd())
|
||||
# 当 skip_env_check 时,默认使用当前目录作为 working_dir
|
||||
if skip_env_check and not args_dict.get("working_dir") and not config_path:
|
||||
# === 解析 working_dir ===
|
||||
# 规则1: working_dir 传入 → 检测 unilabos_data 子目录,已是则不修改
|
||||
# 规则2: 仅 config_path 传入 → 用其父目录作为 working_dir
|
||||
# 规则4: 两者都传入 → 各用各的,但 working_dir 仍做 unilabos_data 子目录检测
|
||||
raw_working_dir = args_dict.get("working_dir")
|
||||
if raw_working_dir:
|
||||
working_dir = os.path.abspath(raw_working_dir)
|
||||
elif config_path and os.path.exists(config_path):
|
||||
working_dir = os.path.dirname(os.path.abspath(config_path))
|
||||
else:
|
||||
working_dir = os.path.abspath(os.getcwd())
|
||||
print_status(f"跳过环境检查模式:使用当前目录作为工作目录 {working_dir}", "info")
|
||||
# 检查当前目录是否有 local_config.py
|
||||
local_config_in_cwd = os.path.join(working_dir, "local_config.py")
|
||||
if os.path.exists(local_config_in_cwd):
|
||||
config_path = local_config_in_cwd
|
||||
|
||||
# unilabos_data 子目录自动检测
|
||||
if os.path.basename(working_dir) != "unilabos_data":
|
||||
unilabos_data_sub = os.path.join(working_dir, "unilabos_data")
|
||||
if os.path.isdir(unilabos_data_sub):
|
||||
working_dir = unilabos_data_sub
|
||||
elif not raw_working_dir and not (config_path and os.path.exists(config_path)):
|
||||
# 未显式指定路径,默认使用 cwd/unilabos_data
|
||||
working_dir = os.path.abspath(os.path.join(os.getcwd(), "unilabos_data"))
|
||||
|
||||
# === 解析 config_path ===
|
||||
if config_path and not os.path.exists(config_path):
|
||||
# config_path 传入但不存在,尝试在 working_dir 中查找
|
||||
candidate = os.path.join(working_dir, "local_config.py")
|
||||
if os.path.exists(candidate):
|
||||
config_path = candidate
|
||||
print_status(f"在工作目录中发现配置文件: {config_path}", "info")
|
||||
else:
|
||||
print_status(
|
||||
f"配置文件 {config_path} 不存在,工作目录 {working_dir} 中也未找到 local_config.py,"
|
||||
f"请通过 --config 传入 local_config.py 文件路径",
|
||||
"error",
|
||||
)
|
||||
os._exit(1)
|
||||
elif not config_path:
|
||||
# 规则3: 未传入 config_path,尝试 working_dir/local_config.py
|
||||
candidate = os.path.join(working_dir, "local_config.py")
|
||||
if os.path.exists(candidate):
|
||||
config_path = candidate
|
||||
print_status(f"发现本地配置文件: {config_path}", "info")
|
||||
else:
|
||||
print_status(f"未指定config路径,可通过 --config 传入 local_config.py 文件路径", "info")
|
||||
elif os.getcwd().endswith("unilabos_data"):
|
||||
working_dir = os.path.abspath(os.getcwd())
|
||||
else:
|
||||
working_dir = os.path.abspath(os.path.join(os.getcwd(), "unilabos_data"))
|
||||
|
||||
if args_dict.get("working_dir"):
|
||||
working_dir = args_dict.get("working_dir", "")
|
||||
if config_path and not os.path.exists(config_path):
|
||||
config_path = os.path.join(working_dir, "local_config.py")
|
||||
if not os.path.exists(config_path):
|
||||
print_status(
|
||||
f"当前工作目录 {working_dir} 未找到local_config.py,请通过 --config 传入 local_config.py 文件路径",
|
||||
"error",
|
||||
print_status(f"您是否为第一次使用?并将当前路径 {working_dir} 作为工作目录? (Y/n)", "info")
|
||||
if check_mode or input() != "n":
|
||||
os.makedirs(working_dir, exist_ok=True)
|
||||
config_path = os.path.join(working_dir, "local_config.py")
|
||||
shutil.copy(
|
||||
os.path.join(os.path.dirname(os.path.dirname(__file__)), "config", "example_config.py"),
|
||||
config_path,
|
||||
)
|
||||
print_status(f"已创建 local_config.py 路径: {config_path}", "info")
|
||||
else:
|
||||
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")):
|
||||
config_path = os.path.join(working_dir, "local_config.py")
|
||||
elif not skip_env_check and not config_path and (
|
||||
not os.path.exists(working_dir) or not os.path.exists(os.path.join(working_dir, "local_config.py"))
|
||||
):
|
||||
print_status(f"未指定config路径,可通过 --config 传入 local_config.py 文件路径", "info")
|
||||
print_status(f"您是否为第一次使用?并将当前路径 {working_dir} 作为工作目录? (Y/n)", "info")
|
||||
if input() != "n":
|
||||
os.makedirs(working_dir, exist_ok=True)
|
||||
config_path = os.path.join(working_dir, "local_config.py")
|
||||
shutil.copy(
|
||||
os.path.join(os.path.dirname(os.path.dirname(__file__)), "config", "example_config.py"), config_path
|
||||
)
|
||||
print_status(f"已创建 local_config.py 路径: {config_path}", "info")
|
||||
else:
|
||||
os._exit(1)
|
||||
|
||||
# 加载配置文件 (check_mode 跳过)
|
||||
print_status(f"当前工作目录为 {working_dir}", "info")
|
||||
@@ -288,15 +440,17 @@ def main():
|
||||
|
||||
if hasattr(BasicConfig, "log_level"):
|
||||
logger.info(f"Log level set to '{BasicConfig.log_level}' from config file.")
|
||||
configure_logger(loglevel=BasicConfig.log_level, working_dir=working_dir)
|
||||
file_path = configure_logger(loglevel=BasicConfig.log_level, working_dir=working_dir)
|
||||
if file_path is not None:
|
||||
logger.info(f"[LOG_FILE] {file_path}")
|
||||
|
||||
if args.addr != parser.get_default("addr"):
|
||||
if args.addr == "test":
|
||||
print_status("使用测试环境地址", "info")
|
||||
HTTPConfig.remote_addr = "https://uni-lab.test.bohrium.com/api/v1"
|
||||
HTTPConfig.remote_addr = "https://leap-lab.test.bohrium.com/api/v1"
|
||||
elif args.addr == "uat":
|
||||
print_status("使用uat环境地址", "info")
|
||||
HTTPConfig.remote_addr = "https://uni-lab.uat.bohrium.com/api/v1"
|
||||
HTTPConfig.remote_addr = "https://leap-lab.uat.bohrium.com/api/v1"
|
||||
elif args.addr == "local":
|
||||
print_status("使用本地环境地址", "info")
|
||||
HTTPConfig.remote_addr = "http://127.0.0.1:48197/api/v1"
|
||||
@@ -332,46 +486,66 @@ def main():
|
||||
BasicConfig.slave_no_host = args_dict.get("slave_no_host", False)
|
||||
BasicConfig.upload_registry = args_dict.get("upload_registry", False)
|
||||
BasicConfig.no_update_feedback = args_dict.get("no_update_feedback", False)
|
||||
BasicConfig.test_mode = args_dict.get("test_mode", False)
|
||||
if BasicConfig.test_mode:
|
||||
print_status("启用测试模式:所有动作将模拟执行,不调用真实硬件", "warning")
|
||||
BasicConfig.extra_resource = args_dict.get("extra_resource", False)
|
||||
if BasicConfig.extra_resource:
|
||||
print_status("启用额外资源加载:将加载lab_开头的labware资源定义", "info")
|
||||
BasicConfig.communication_protocol = "websocket"
|
||||
machine_name = os.popen("hostname").read().strip()
|
||||
machine_name = platform.node()
|
||||
machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name])
|
||||
BasicConfig.machine_name = machine_name
|
||||
BasicConfig.vis_2d_enable = args_dict["2d_vis"]
|
||||
BasicConfig.check_mode = check_mode
|
||||
|
||||
from unilabos.resources.graphio import (
|
||||
read_node_link_json,
|
||||
read_graphml,
|
||||
dict_from_graph,
|
||||
)
|
||||
from unilabos.app.communication import get_communication_client
|
||||
from unilabos.registry.registry import build_registry
|
||||
from unilabos.app.backend import start_backend
|
||||
from unilabos.app.web import http_client
|
||||
from unilabos.app.web import start_server
|
||||
from unilabos.app.register import register_devices_and_resources
|
||||
from unilabos.resources.graphio import modify_to_backend_format
|
||||
from unilabos.resources.resource_tracker import ResourceTreeSet, ResourceDict
|
||||
|
||||
# 显示启动横幅
|
||||
print_unilab_banner(args_dict)
|
||||
|
||||
# 注册表 - check_mode 时强制启用 complete_registry
|
||||
# Step 0: AST 分析优先 + YAML 注册表加载
|
||||
# check_mode 和 upload_registry 都会执行实际 import 验证
|
||||
devices_dirs = args_dict.get("devices", None)
|
||||
complete_registry = args_dict.get("complete_registry", False) or check_mode
|
||||
lab_registry = build_registry(args_dict["registry_path"], complete_registry, BasicConfig.upload_registry)
|
||||
external_only = args_dict.get("external_devices_only", False)
|
||||
lab_registry = build_registry(
|
||||
registry_paths=args_dict["registry_path"],
|
||||
devices_dirs=devices_dirs,
|
||||
upload_registry=BasicConfig.upload_registry,
|
||||
check_mode=check_mode,
|
||||
complete_registry=complete_registry,
|
||||
external_only=external_only,
|
||||
)
|
||||
|
||||
# Check mode: complete_registry 完成后直接退出,git diff 检测由 CI workflow 执行
|
||||
# Check mode: 注册表验证完成后直接退出
|
||||
if check_mode:
|
||||
print_status("Check mode: complete_registry 完成,退出", "info")
|
||||
device_count = len(lab_registry.device_type_registry)
|
||||
resource_count = len(lab_registry.resource_type_registry)
|
||||
print_status(f"Check mode: 注册表验证完成 ({device_count} 设备, {resource_count} 资源),退出", "info")
|
||||
os._exit(0)
|
||||
|
||||
# 以下导入依赖 ROS2 环境,check_mode 已退出不需要
|
||||
from unilabos.resources.graphio import (
|
||||
read_node_link_json,
|
||||
read_graphml,
|
||||
dict_from_graph,
|
||||
modify_to_backend_format,
|
||||
)
|
||||
from unilabos.app.communication import get_communication_client
|
||||
from unilabos.app.backend import start_backend
|
||||
from unilabos.app.web import http_client
|
||||
from unilabos.app.web import start_server
|
||||
from unilabos.app.register import register_devices_and_resources
|
||||
from unilabos.resources.resource_tracker import ResourceTreeSet, ResourceDict
|
||||
|
||||
# Step 1: 上传全部注册表到服务端,同步保存到 unilabos_data
|
||||
if BasicConfig.upload_registry:
|
||||
# 设备注册到服务端 - 需要 ak 和 sk
|
||||
if BasicConfig.ak and BasicConfig.sk:
|
||||
print_status("开始注册设备到服务端...", "info")
|
||||
# print_status("开始注册设备到服务端...", "info")
|
||||
try:
|
||||
register_devices_and_resources(lab_registry)
|
||||
print_status("设备注册完成", "info")
|
||||
# print_status("设备注册完成", "info")
|
||||
except Exception as e:
|
||||
print_status(f"设备注册失败: {e}", "error")
|
||||
else:
|
||||
@@ -388,7 +562,7 @@ def main():
|
||||
os._exit(0)
|
||||
|
||||
if not BasicConfig.ak or not BasicConfig.sk:
|
||||
print_status("后续运行必须拥有一个实验室,请前往 https://uni-lab.bohrium.com 注册实验室!", "warning")
|
||||
print_status("后续运行必须拥有一个实验室,请前往 https://leap-lab.bohrium.com 注册实验室!", "warning")
|
||||
os._exit(1)
|
||||
graph: nx.Graph
|
||||
resource_tree_set: ResourceTreeSet
|
||||
@@ -456,12 +630,18 @@ def main():
|
||||
continue
|
||||
|
||||
# 如果从远端获取了物料信息,则与本地物料进行同步
|
||||
if request_startup_json and "nodes" in request_startup_json:
|
||||
# 仅在本地文件模式下有意义:本地文件只含设备结构,远端有已保存的物料,需要 merge
|
||||
# 远端模式下 resource_tree_set 与 request_startup_json 来自同一份数据,merge 为空操作
|
||||
if file_path is not None and request_startup_json and "nodes" in request_startup_json:
|
||||
print_status("开始同步远端物料到本地...", "info")
|
||||
remote_tree_set = ResourceTreeSet.from_raw_dict_list(request_startup_json["nodes"])
|
||||
resource_tree_set.merge_remote_resources(remote_tree_set)
|
||||
print_status("远端物料同步完成", "info")
|
||||
|
||||
# 第二次设备包依赖检查:云端物料同步后,community 包可能引入新的 requirements
|
||||
# TODO: 当 community device package 功能上线后,在这里调用
|
||||
# install_requirements_txt(community_pkg_path / "requirements.txt", label="community.xxx")
|
||||
|
||||
# 使用 ResourceTreeSet 代替 list
|
||||
args_dict["resources_config"] = resource_tree_set
|
||||
args_dict["devices_config"] = resource_tree_set
|
||||
@@ -553,6 +733,10 @@ def main():
|
||||
open_browser=not args_dict["disable_browser"],
|
||||
port=BasicConfig.port,
|
||||
)
|
||||
if restart_requested:
|
||||
print_status("[Main] Restart requested, cleaning up...", "info")
|
||||
cleanup_for_restart()
|
||||
os._exit(RESTART_EXIT_CODE)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -54,11 +54,12 @@ class JobAddReq(BaseModel):
|
||||
action_type: str = Field(
|
||||
examples=["unilabos_msgs.action._str_single_input.StrSingleInput"], description="action type", default=""
|
||||
)
|
||||
sample_material: dict = Field(examples=[{"string": "string"}], description="sample uuid to material uuid", default_factory=dict)
|
||||
sample_material: dict = Field(examples=[{"string": "string"}], description="sample uuid to material uuid")
|
||||
action_args: dict = Field(examples=[{"string": "string"}], description="action arguments", default_factory=dict)
|
||||
task_id: str = Field(examples=["task_id"], description="task uuid (auto-generated if empty)", default="")
|
||||
job_id: str = Field(examples=["job_id"], description="goal uuid (auto-generated if empty)", default="")
|
||||
node_id: str = Field(examples=["node_id"], description="node uuid", default="")
|
||||
notebook_id: str = Field(examples=["notebook_id"], description="notebook uuid", default="")
|
||||
server_info: dict = Field(
|
||||
examples=[{"send_timestamp": 1717000000.0}],
|
||||
description="server info (auto-generated if empty)",
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import json
|
||||
import time
|
||||
from typing import Optional, Tuple, Dict, Any
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
|
||||
from unilabos.utils.log import logger
|
||||
from unilabos.utils.type_check import TypeEncoder
|
||||
from unilabos.utils.tools import normalize_json as _normalize_device
|
||||
|
||||
|
||||
def register_devices_and_resources(lab_registry, gather_only=False) -> Optional[Tuple[Dict[str, Any], Dict[str, Any]]]:
|
||||
@@ -11,50 +10,63 @@ def register_devices_and_resources(lab_registry, gather_only=False) -> Optional[
|
||||
注册设备和资源到服务器(仅支持HTTP)
|
||||
"""
|
||||
|
||||
# 注册资源信息 - 使用HTTP方式
|
||||
from unilabos.app.web.client import http_client
|
||||
|
||||
logger.info("[UniLab Register] 开始注册设备和资源...")
|
||||
|
||||
# 注册设备信息
|
||||
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']}")
|
||||
devices_to_register[device_info["id"]] = _normalize_device(device_info)
|
||||
logger.trace(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']}")
|
||||
logger.trace(f"[UniLab Register] 收集资源: {resource_info['id']}")
|
||||
|
||||
if gather_only:
|
||||
return devices_to_register, resources_to_register
|
||||
# 注册设备
|
||||
|
||||
if devices_to_register:
|
||||
try:
|
||||
start_time = time.time()
|
||||
response = http_client.resource_registry({"resources": list(devices_to_register.values())})
|
||||
response = http_client.resource_registry(
|
||||
{"resources": list(devices_to_register.values())},
|
||||
tag="device_registry",
|
||||
)
|
||||
cost_time = time.time() - start_time
|
||||
if response.status_code in [200, 201]:
|
||||
logger.info(f"[UniLab Register] 成功注册 {len(devices_to_register)} 个设备 {cost_time}ms")
|
||||
res_data = response.json() if response.status_code == 200 else {}
|
||||
skipped = res_data.get("data", {}).get("skipped", False)
|
||||
if skipped:
|
||||
logger.info(
|
||||
f"[UniLab Register] 设备注册跳过(内容未变化)"
|
||||
f" {len(devices_to_register)} 个 {cost_time:.3f}s"
|
||||
)
|
||||
elif response.status_code in [200, 201]:
|
||||
logger.info(f"[UniLab Register] 成功注册 {len(devices_to_register)} 个设备 {cost_time:.3f}s")
|
||||
else:
|
||||
logger.error(f"[UniLab Register] 设备注册失败: {response.status_code}, {response.text} {cost_time}ms")
|
||||
logger.error(f"[UniLab Register] 设备注册失败: {response.status_code}, {response.text} {cost_time:.3f}s")
|
||||
except Exception as e:
|
||||
logger.error(f"[UniLab Register] 设备注册异常: {e}")
|
||||
|
||||
# 注册资源
|
||||
if resources_to_register:
|
||||
try:
|
||||
start_time = time.time()
|
||||
response = http_client.resource_registry({"resources": list(resources_to_register.values())})
|
||||
response = http_client.resource_registry(
|
||||
{"resources": list(resources_to_register.values())},
|
||||
tag="resource_registry",
|
||||
)
|
||||
cost_time = time.time() - start_time
|
||||
if response.status_code in [200, 201]:
|
||||
logger.info(f"[UniLab Register] 成功注册 {len(resources_to_register)} 个资源 {cost_time}ms")
|
||||
res_data = response.json() if response.status_code == 200 else {}
|
||||
skipped = res_data.get("data", {}).get("skipped", False)
|
||||
if skipped:
|
||||
logger.info(
|
||||
f"[UniLab Register] 资源注册跳过(内容未变化)"
|
||||
f" {len(resources_to_register)} 个 {cost_time:.3f}s"
|
||||
)
|
||||
elif response.status_code in [200, 201]:
|
||||
logger.info(f"[UniLab Register] 成功注册 {len(resources_to_register)} 个资源 {cost_time:.3f}s")
|
||||
else:
|
||||
logger.error(f"[UniLab Register] 资源注册失败: {response.status_code}, {response.text} {cost_time}ms")
|
||||
logger.error(f"[UniLab Register] 资源注册失败: {response.status_code}, {response.text} {cost_time:.3f}s")
|
||||
except Exception as e:
|
||||
logger.error(f"[UniLab Register] 资源注册异常: {e}")
|
||||
|
||||
logger.info("[UniLab Register] 设备和资源注册完成.")
|
||||
|
||||
@@ -10,29 +10,170 @@ import shutil
|
||||
import sys
|
||||
|
||||
|
||||
_PATCH_MARKER = "# UniLabOS DLL Patch"
|
||||
_PATCH_END_MARKER = "# End UniLabOS DLL Patch"
|
||||
|
||||
# 75 = EX_TEMPFAIL: 临时失败、重试即可,避免与业务退出码冲突
|
||||
_RESTART_EXIT_CODE = 75
|
||||
|
||||
|
||||
def _build_dll_patch(lib_bin: str, preload_pyd: str = "") -> str:
|
||||
"""生成一段加在目标文件顶部的 DLL 加载补丁源码。
|
||||
|
||||
- 始终把 ``lib_bin`` 加入 DLL 搜索路径,并把 handle 挂在模块属性上,
|
||||
防止 GC 清掉搜索路径(``os.add_dll_directory`` 的句柄被回收时
|
||||
目录会被移除)。
|
||||
- 可选地用 ``ctypes.CDLL`` 预加载一个 .pyd,把它的依赖 DLL 提前装入
|
||||
进程内存,作为 ``rclpy._rclpy_pybind11`` 这类首次加载点的兜底。
|
||||
"""
|
||||
# 用 repr() 序列化路径:Python 解析 repr 的结果会还原成原始字符串,
|
||||
# 不需要也不能再叠加 raw-string 前缀(叠了反而会让 \\ 变成两个反斜杠)。
|
||||
lines = [
|
||||
_PATCH_MARKER,
|
||||
"import os as _ulab_os",
|
||||
f"_ulab_p = {lib_bin!r}",
|
||||
'if hasattr(_ulab_os, "add_dll_directory") and _ulab_os.path.isdir(_ulab_p):',
|
||||
" try: _UNILAB_DLL_HANDLE = _ulab_os.add_dll_directory(_ulab_p)",
|
||||
" except Exception: _UNILAB_DLL_HANDLE = None",
|
||||
]
|
||||
if preload_pyd:
|
||||
lines.extend(
|
||||
[
|
||||
"import ctypes as _ulab_ctypes",
|
||||
f"try: _ulab_ctypes.CDLL({preload_pyd!r})",
|
||||
"except Exception: pass",
|
||||
]
|
||||
)
|
||||
lines.append(_PATCH_END_MARKER)
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def _apply_dll_patch(file_path: str, lib_bin: str, preload_pyd: str = "") -> bool:
|
||||
"""把 DLL 补丁前置到 ``file_path``。文件不存在或已打过补丁则返回 False。"""
|
||||
if not os.path.isfile(file_path):
|
||||
return False
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
if _PATCH_MARKER in content:
|
||||
return False
|
||||
shutil.copy2(file_path, file_path + ".bak")
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write(_build_dll_patch(lib_bin, preload_pyd) + content)
|
||||
return True
|
||||
|
||||
|
||||
def _print_restart_banner(patched_files):
|
||||
"""打印重启提示并以 EX_TEMPFAIL 退出。
|
||||
|
||||
- 不使用 ANSI 颜色码:Windows 旧版 cmd / PowerShell 5 默认不开 VT 处理,
|
||||
会把 ``\\033[1;33m`` 当做字面字符显示,反而让用户看不到正文。
|
||||
- 同时写入 stderr 与 stdout:某些上层 launcher / supervisor 只重定向
|
||||
其中一路,写两遍能保证用户至少看到一份。
|
||||
- 写入前防御性把流切到 UTF-8 with replace:``main.py`` 里已经做过一次,
|
||||
但本模块也可能被绕过 ``main.py`` 的代码路径直接 import;reconfigure
|
||||
失败也只是退回 errors=replace,不影响整体流程。
|
||||
"""
|
||||
if sys.platform == "win32":
|
||||
for _stream in (sys.stdout, sys.stderr):
|
||||
try:
|
||||
_stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[attr-defined]
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
|
||||
bar = "#" * 78
|
||||
files_lines = [f"[UniLabOS] - {p}" for p in patched_files]
|
||||
body = "\n".join(
|
||||
[
|
||||
"",
|
||||
bar,
|
||||
bar,
|
||||
"##",
|
||||
"## [UniLabOS] Windows + conda 下检测到 DLL 加载失败,已自动打补丁。",
|
||||
"## [UniLabOS] DLL load failure detected on Windows + conda;",
|
||||
"## [UniLabOS] the following files have been auto-patched:",
|
||||
"##",
|
||||
*[f"## {line}" for line in files_lines],
|
||||
"##",
|
||||
"## [UniLabOS] 当前进程的 rclpy 状态已损坏,补丁需要在新进程才生效。",
|
||||
"## [UniLabOS] The current process is unusable; the patch only takes",
|
||||
"## [UniLabOS] effect on a fresh process.",
|
||||
"##",
|
||||
"## >>> 请重新运行刚才的命令 / Please re-run the same command. <<<",
|
||||
"##",
|
||||
bar,
|
||||
bar,
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
for stream in (sys.stderr, sys.stdout):
|
||||
try:
|
||||
stream.write(body)
|
||||
stream.flush()
|
||||
except Exception:
|
||||
try:
|
||||
print(body, file=stream)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
sys.exit(_RESTART_EXIT_CODE)
|
||||
|
||||
|
||||
def patch_rclpy_dll_windows():
|
||||
"""在 Windows + conda 环境下为 rclpy 打 DLL 加载补丁"""
|
||||
"""在 Windows + conda 环境下修复 rclpy / rosidl typesupport 的 DLL 加载。
|
||||
|
||||
背景:conda 安装的 ros 系列包,其原生扩展依赖 ``$CONDA_PREFIX/Library/bin``
|
||||
下的 DLL;只有 conda 环境被正确激活、且 PATH 中含 ``Library/bin`` 时,
|
||||
``os.add_dll_directory`` 才能找到它们。当从快捷方式 / IDE / 子进程 /
|
||||
没激活的 shell 启动 ``unilab`` 时,会出现 ``DLL load failed``。
|
||||
|
||||
本函数会:
|
||||
1) 修补 ``rclpy/impl/implementation_singleton.py`` —— rclpy 自身的 C 扩展入口;
|
||||
2) 修补 ``rpyutils/add_dll_directories.py`` —— 所有 ``*_s__rosidl_typesupport_c.pyd``
|
||||
(``geometry_msgs`` / ``std_msgs`` / ``sensor_msgs`` 等)的统一加载入口。
|
||||
|
||||
打完补丁后**必须重启进程**才能生效(当前进程的 rclpy 已经发生过
|
||||
``ImportError``,子模块仍处于损坏状态)。因此函数会主动退出,并在
|
||||
stdout/stderr 同时打印明显的重启提示,避免用户被后续报错淹没。
|
||||
"""
|
||||
if sys.platform != "win32" or not os.environ.get("CONDA_PREFIX"):
|
||||
return
|
||||
|
||||
try:
|
||||
import rclpy
|
||||
import rclpy # noqa: F401
|
||||
|
||||
return
|
||||
except ImportError as e:
|
||||
if not str(e).startswith("DLL load failed"):
|
||||
return
|
||||
|
||||
cp = os.environ["CONDA_PREFIX"]
|
||||
impl = os.path.join(cp, "Lib", "site-packages", "rclpy", "impl", "implementation_singleton.py")
|
||||
pyd = glob.glob(os.path.join(cp, "Lib", "site-packages", "rclpy", "_rclpy_pybind11*.pyd"))
|
||||
if not os.path.exists(impl) or not pyd:
|
||||
lib_bin = os.path.join(cp, "Library", "bin")
|
||||
site_packages = os.path.join(cp, "Lib", "site-packages")
|
||||
if not os.path.isdir(lib_bin):
|
||||
return
|
||||
with open(impl, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
lib_bin = os.path.join(cp, "Library", "bin").replace("\\", "/")
|
||||
patch = f'# UniLabOS DLL Patch\nimport os,ctypes\nos.add_dll_directory("{lib_bin}") if hasattr(os,"add_dll_directory") else None\ntry: ctypes.CDLL("{pyd[0].replace(chr(92),"/")}")\nexcept: pass\n# End Patch\n'
|
||||
shutil.copy2(impl, impl + ".bak")
|
||||
with open(impl, "w", encoding="utf-8") as f:
|
||||
f.write(patch + content)
|
||||
|
||||
patched = []
|
||||
|
||||
# 1) rclpy 自身的入口
|
||||
rclpy_impl = os.path.join(site_packages, "rclpy", "impl", "implementation_singleton.py")
|
||||
rclpy_pyd_matches = glob.glob(os.path.join(site_packages, "rclpy", "_rclpy_pybind11*.pyd"))
|
||||
rclpy_pyd = rclpy_pyd_matches[0] if rclpy_pyd_matches else ""
|
||||
if rclpy_pyd and _apply_dll_patch(rclpy_impl, lib_bin, preload_pyd=rclpy_pyd):
|
||||
patched.append(rclpy_impl)
|
||||
|
||||
# 2) rpyutils —— 所有 rosidl typesupport pyd 的加载点;放在 rclpy 之后
|
||||
# 例:geometry_msgs/geometry_msgs_s__rosidl_typesupport_c.pyd
|
||||
rpyutils_dll = os.path.join(site_packages, "rpyutils", "add_dll_directories.py")
|
||||
if _apply_dll_patch(rpyutils_dll, lib_bin):
|
||||
patched.append(rpyutils_dll)
|
||||
|
||||
if not patched:
|
||||
# 已经打过补丁但 rclpy 仍然加载失败:原因不是缺 DLL 搜索路径,
|
||||
# 不要再次打补丁污染文件,让上层看到真实的 ImportError。
|
||||
return
|
||||
|
||||
_print_restart_banner(patched)
|
||||
|
||||
|
||||
patch_rclpy_dll_windows()
|
||||
|
||||
@@ -1052,7 +1052,7 @@ async def handle_file_import(websocket: WebSocket, request_data: dict):
|
||||
"result": {},
|
||||
"schema": lab_registry._generate_unilab_json_command_schema(v["args"], k),
|
||||
"goal_default": {i["name"]: i["default"] for i in v["args"]},
|
||||
"handles": [],
|
||||
"handles": {},
|
||||
}
|
||||
# 不生成已配置action的动作
|
||||
for k, v in enhanced_info["action_methods"].items()
|
||||
@@ -1340,5 +1340,5 @@ def setup_api_routes(app):
|
||||
# 启动广播任务
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
asyncio.create_task(broadcast_device_status())
|
||||
asyncio.create_task(broadcast_status_page_data())
|
||||
asyncio.create_task(broadcast_device_status(), name="web-api-startup-device")
|
||||
asyncio.create_task(broadcast_status_page_data(), name="web-api-startup-status")
|
||||
|
||||
@@ -3,11 +3,13 @@ HTTP客户端模块
|
||||
|
||||
提供与远程服务器通信的客户端功能,只有host需要用
|
||||
"""
|
||||
|
||||
import gzip
|
||||
import json
|
||||
import os
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
from unilabos.utils.tools import fast_dumps as _fast_dumps, fast_dumps_pretty as _fast_dumps_pretty
|
||||
|
||||
import requests
|
||||
from unilabos.resources.resource_tracker import ResourceTreeSet
|
||||
from unilabos.utils.log import info
|
||||
@@ -34,6 +36,9 @@ class HTTPClient:
|
||||
auth_secret = BasicConfig.auth_secret()
|
||||
self.auth = auth_secret
|
||||
info(f"正在使用ak sk作为授权信息:[{auth_secret}]")
|
||||
# 复用 TCP/TLS 连接,避免每次请求重新握手
|
||||
self._session = requests.Session()
|
||||
self._session.headers.update({"Authorization": f"Lab {self.auth}"})
|
||||
info(f"HTTPClient 初始化完成: remote_addr={self.remote_addr}")
|
||||
|
||||
def resource_edge_add(self, resources: List[Dict[str, Any]]) -> requests.Response:
|
||||
@@ -46,7 +51,7 @@ class HTTPClient:
|
||||
Returns:
|
||||
Response: API响应对象
|
||||
"""
|
||||
response = requests.post(
|
||||
response = self._session.post(
|
||||
f"{self.remote_addr}/edge/material/edge",
|
||||
json={
|
||||
"edges": resources,
|
||||
@@ -73,25 +78,28 @@ class HTTPClient:
|
||||
Returns:
|
||||
Dict[str, str]: 旧UUID到新UUID的映射关系 {old_uuid: new_uuid}
|
||||
"""
|
||||
with open(os.path.join(BasicConfig.working_dir, "req_resource_tree_add.json"), "w", encoding="utf-8") as f:
|
||||
payload = {"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid}
|
||||
f.write(json.dumps(payload, indent=4))
|
||||
# 从序列化数据中提取所有节点的UUID(保存旧UUID)
|
||||
# dump() 只调用一次,复用给文件保存和 HTTP 请求
|
||||
nodes_info = [x for xs in resources.dump() for x in xs]
|
||||
old_uuids = {n.res_content.uuid: n for n in resources.all_nodes}
|
||||
payload = {"nodes": nodes_info, "mount_uuid": mount_uuid}
|
||||
body_bytes = _fast_dumps(payload)
|
||||
with open(os.path.join(BasicConfig.working_dir, "req_resource_tree_add.json"), "wb") as f:
|
||||
f.write(_fast_dumps_pretty(payload))
|
||||
http_headers = {"Content-Type": "application/json"}
|
||||
if not self.initialized or first_add:
|
||||
self.initialized = True
|
||||
info(f"首次添加资源,当前远程地址: {self.remote_addr}")
|
||||
response = requests.post(
|
||||
response = self._session.post(
|
||||
f"{self.remote_addr}/edge/material",
|
||||
json={"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
data=body_bytes,
|
||||
headers=http_headers,
|
||||
timeout=60,
|
||||
)
|
||||
else:
|
||||
response = requests.put(
|
||||
response = self._session.put(
|
||||
f"{self.remote_addr}/edge/material",
|
||||
json={"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
data=body_bytes,
|
||||
headers=http_headers,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
@@ -109,6 +117,7 @@ class HTTPClient:
|
||||
uuid_mapping[i["uuid"]] = i["cloud_uuid"]
|
||||
else:
|
||||
logger.error(f"添加物料失败: {response.text}")
|
||||
logger.trace(f"添加物料失败: {nodes_info}")
|
||||
for u, n in old_uuids.items():
|
||||
if u in uuid_mapping:
|
||||
n.res_content.uuid = uuid_mapping[u]
|
||||
@@ -129,7 +138,7 @@ class HTTPClient:
|
||||
"""
|
||||
with open(os.path.join(BasicConfig.working_dir, "req_resource_tree_get.json"), "w", encoding="utf-8") as f:
|
||||
f.write(json.dumps({"uuids": uuid_list, "with_children": with_children}, indent=4))
|
||||
response = requests.post(
|
||||
response = self._session.post(
|
||||
f"{self.remote_addr}/edge/material/query",
|
||||
json={"uuids": uuid_list, "with_children": with_children},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
@@ -143,6 +152,7 @@ class HTTPClient:
|
||||
logger.error(f"查询物料失败: {response.text}")
|
||||
else:
|
||||
data = res["data"]["nodes"]
|
||||
logger.trace(f"resource_tree_get查询到物料: {data}")
|
||||
return data
|
||||
else:
|
||||
logger.error(f"查询物料失败: {response.text}")
|
||||
@@ -160,14 +170,14 @@ class HTTPClient:
|
||||
if not self.initialized:
|
||||
self.initialized = True
|
||||
info(f"首次添加资源,当前远程地址: {self.remote_addr}")
|
||||
response = requests.post(
|
||||
response = self._session.post(
|
||||
f"{self.remote_addr}/lab/material",
|
||||
json={"nodes": resources},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=100,
|
||||
)
|
||||
else:
|
||||
response = requests.put(
|
||||
response = self._session.put(
|
||||
f"{self.remote_addr}/lab/material",
|
||||
json={"nodes": resources},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
@@ -194,7 +204,7 @@ class HTTPClient:
|
||||
"""
|
||||
with open(os.path.join(BasicConfig.working_dir, "req_resource_get.json"), "w", encoding="utf-8") as f:
|
||||
f.write(json.dumps({"id": id, "with_children": with_children}, indent=4))
|
||||
response = requests.get(
|
||||
response = self._session.get(
|
||||
f"{self.remote_addr}/lab/material",
|
||||
params={"id": id, "with_children": with_children},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
@@ -235,14 +245,14 @@ class HTTPClient:
|
||||
if not self.initialized:
|
||||
self.initialized = True
|
||||
info(f"首次添加资源,当前远程地址: {self.remote_addr}")
|
||||
response = requests.post(
|
||||
response = self._session.post(
|
||||
f"{self.remote_addr}/lab/material",
|
||||
json={"nodes": resources},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=100,
|
||||
)
|
||||
else:
|
||||
response = requests.put(
|
||||
response = self._session.put(
|
||||
f"{self.remote_addr}/lab/material",
|
||||
json={"nodes": resources},
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
@@ -272,7 +282,7 @@ class HTTPClient:
|
||||
with open(file_path, "rb") as file:
|
||||
files = {"files": file}
|
||||
logger.info(f"上传文件: {file_path} 到 {scene}")
|
||||
response = requests.post(
|
||||
response = self._session.post(
|
||||
f"{self.remote_addr}/api/account/file_upload/{scene}",
|
||||
files=files,
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
@@ -280,22 +290,54 @@ class HTTPClient:
|
||||
)
|
||||
return response
|
||||
|
||||
def resource_registry(self, registry_data: Dict[str, Any] | List[Dict[str, Any]]) -> requests.Response:
|
||||
def resource_registry(
|
||||
self, registry_data: Dict[str, Any] | List[Dict[str, Any]], tag: str = "registry",
|
||||
) -> requests.Response:
|
||||
"""
|
||||
注册资源到服务器
|
||||
注册资源到服务器,同步保存请求/响应到 unilabos_data
|
||||
|
||||
Args:
|
||||
registry_data: 注册表数据,格式为 {resource_id: resource_info} / [{resource_info}]
|
||||
tag: 保存文件的标签后缀 (如 "device_registry" / "resource_registry")
|
||||
|
||||
Returns:
|
||||
Response: API响应对象
|
||||
"""
|
||||
response = requests.post(
|
||||
# 序列化一次,同时用于保存和发送
|
||||
json_bytes = _fast_dumps(registry_data)
|
||||
|
||||
# 保存请求数据到 unilabos_data
|
||||
req_path = os.path.join(BasicConfig.working_dir, f"req_{tag}_upload.json")
|
||||
try:
|
||||
os.makedirs(BasicConfig.working_dir, exist_ok=True)
|
||||
with open(req_path, "wb") as f:
|
||||
f.write(_fast_dumps_pretty(registry_data))
|
||||
logger.trace(f"注册表请求数据已保存: {req_path}")
|
||||
except Exception as e:
|
||||
logger.warning(f"保存注册表请求数据失败: {e}")
|
||||
|
||||
compressed_body = gzip.compress(json_bytes)
|
||||
headers = {
|
||||
"Authorization": f"Lab {self.auth}",
|
||||
"Content-Type": "application/json",
|
||||
"Content-Encoding": "gzip",
|
||||
}
|
||||
response = self._session.post(
|
||||
f"{self.remote_addr}/lab/resource",
|
||||
json=registry_data,
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
data=compressed_body,
|
||||
headers=headers,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
# 保存响应数据到 unilabos_data
|
||||
res_path = os.path.join(BasicConfig.working_dir, f"res_{tag}_upload.json")
|
||||
try:
|
||||
with open(res_path, "w", encoding="utf-8") as f:
|
||||
f.write(f"{response.status_code}\n{response.text}")
|
||||
logger.trace(f"注册表响应数据已保存: {res_path}")
|
||||
except Exception as e:
|
||||
logger.warning(f"保存注册表响应数据失败: {e}")
|
||||
|
||||
if response.status_code not in [200, 201]:
|
||||
logger.error(f"注册资源失败: {response.status_code}, {response.text}")
|
||||
if response.status_code == 200:
|
||||
@@ -314,7 +356,7 @@ class HTTPClient:
|
||||
Returns:
|
||||
Response: API响应对象
|
||||
"""
|
||||
response = requests.get(
|
||||
response = self._session.get(
|
||||
f"{self.remote_addr}/edge/material/download",
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=(3, 30),
|
||||
@@ -343,9 +385,10 @@ class HTTPClient:
|
||||
edges: List[Dict[str, Any]],
|
||||
tags: Optional[List[str]] = None,
|
||||
published: bool = False,
|
||||
description: str = "",
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
导入工作流到服务器
|
||||
导入工作流到服务器,如果 published 为 True,则额外发起发布请求
|
||||
|
||||
Args:
|
||||
name: 工作流名称(顶层)
|
||||
@@ -355,6 +398,7 @@ class HTTPClient:
|
||||
edges: 工作流边列表
|
||||
tags: 工作流标签列表,默认为空列表
|
||||
published: 是否发布工作流,默认为False
|
||||
description: 工作流描述,发布时使用
|
||||
|
||||
Returns:
|
||||
Dict: API响应数据,包含 code 和 data (uuid, name)
|
||||
@@ -367,14 +411,13 @@ class HTTPClient:
|
||||
"nodes": nodes,
|
||||
"edges": edges,
|
||||
"tags": tags if tags is not None else [],
|
||||
"published": published,
|
||||
},
|
||||
}
|
||||
# 保存请求到文件
|
||||
with open(os.path.join(BasicConfig.working_dir, "req_workflow_upload.json"), "w", encoding="utf-8") as f:
|
||||
f.write(json.dumps(payload, indent=4, ensure_ascii=False))
|
||||
|
||||
response = requests.post(
|
||||
response = self._session.post(
|
||||
f"{self.remote_addr}/lab/workflow/owner/import",
|
||||
json=payload,
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
@@ -388,11 +431,51 @@ class HTTPClient:
|
||||
res = response.json()
|
||||
if "code" in res and res["code"] != 0:
|
||||
logger.error(f"导入工作流失败: {response.text}")
|
||||
return res
|
||||
# 导入成功后,如果需要发布则额外发起发布请求
|
||||
if published:
|
||||
imported_uuid = res.get("data", {}).get("uuid", workflow_uuid)
|
||||
publish_res = self.workflow_publish(imported_uuid, description)
|
||||
res["publish_result"] = publish_res
|
||||
return res
|
||||
else:
|
||||
logger.error(f"导入工作流失败: {response.status_code}, {response.text}")
|
||||
return {"code": response.status_code, "message": response.text}
|
||||
|
||||
def workflow_publish(self, workflow_uuid: str, description: str = "") -> Dict[str, Any]:
|
||||
"""
|
||||
发布工作流
|
||||
|
||||
Args:
|
||||
workflow_uuid: 工作流UUID
|
||||
description: 工作流描述
|
||||
|
||||
Returns:
|
||||
Dict: API响应数据
|
||||
"""
|
||||
payload = {
|
||||
"uuid": workflow_uuid,
|
||||
"description": description,
|
||||
"published": True,
|
||||
}
|
||||
logger.info(f"正在发布工作流: {workflow_uuid}")
|
||||
response = requests.patch(
|
||||
f"{self.remote_addr}/lab/workflow/owner",
|
||||
json=payload,
|
||||
headers={"Authorization": f"Lab {self.auth}"},
|
||||
timeout=60,
|
||||
)
|
||||
if response.status_code == 200:
|
||||
res = response.json()
|
||||
if "code" in res and res["code"] != 0:
|
||||
logger.error(f"发布工作流失败: {response.text}")
|
||||
else:
|
||||
logger.info(f"工作流发布成功: {workflow_uuid}")
|
||||
return res
|
||||
else:
|
||||
logger.error(f"发布工作流失败: {response.status_code}, {response.text}")
|
||||
return {"code": response.status_code, "message": response.text}
|
||||
|
||||
|
||||
# 创建默认客户端实例
|
||||
http_client = HTTPClient()
|
||||
|
||||
@@ -320,6 +320,7 @@ def job_add(req: JobAddReq) -> JobData:
|
||||
action_name=action_name,
|
||||
task_id=task_id,
|
||||
job_id=job_id,
|
||||
notebook_id=req.notebook_id,
|
||||
device_action_key=device_action_key,
|
||||
)
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ def setup_server() -> FastAPI:
|
||||
# 设置页面路由
|
||||
try:
|
||||
setup_web_pages(pages)
|
||||
info("[Web] 已加载Web UI模块")
|
||||
# info("[Web] 已加载Web UI模块")
|
||||
except ImportError as e:
|
||||
info(f"[Web] 未找到Web页面模块: {str(e)}")
|
||||
except Exception as e:
|
||||
@@ -138,7 +138,7 @@ def start_server(host: str = "0.0.0.0", port: int = 8002, open_browser: bool = T
|
||||
server_thread = threading.Thread(target=server.run, daemon=True, name="uvicorn_server")
|
||||
server_thread.start()
|
||||
|
||||
info("[Web] Server started, monitoring for restart requests...")
|
||||
# info("[Web] Server started, monitoring for restart requests...")
|
||||
|
||||
# 监控重启标志
|
||||
import unilabos.app.main as main_module
|
||||
|
||||
@@ -26,6 +26,7 @@ from enum import Enum
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from unilabos.app.model import JobAddReq
|
||||
from unilabos.resources.resource_tracker import ResourceDictType
|
||||
from unilabos.ros.nodes.presets.host_node import HostNode
|
||||
from unilabos.utils.type_check import serialize_result_info
|
||||
from unilabos.app.communication import BaseCommunicationClient
|
||||
@@ -58,6 +59,7 @@ class QueueItem:
|
||||
action_name: str
|
||||
task_id: str
|
||||
job_id: str
|
||||
notebook_id: str
|
||||
device_action_key: str
|
||||
next_run_time: float = 0 # 下次执行时间戳
|
||||
retry_count: int = 0 # 重试次数
|
||||
@@ -70,12 +72,14 @@ class JobInfo:
|
||||
job_id: str
|
||||
task_id: str
|
||||
device_id: str
|
||||
notebook_id: str
|
||||
action_name: str
|
||||
device_action_key: str
|
||||
status: JobStatus
|
||||
start_time: float
|
||||
last_update_time: float = field(default_factory=time.time)
|
||||
ready_timeout: Optional[float] = None # READY状态的超时时间
|
||||
always_free: bool = False # 是否为永久闲置动作(不受排队限制)
|
||||
|
||||
def update_timestamp(self):
|
||||
"""更新最后更新时间"""
|
||||
@@ -127,6 +131,15 @@ class DeviceActionManager:
|
||||
# 总是将job添加到all_jobs中
|
||||
self.all_jobs[job_info.job_id] = job_info
|
||||
|
||||
# always_free的动作不受排队限制,直接设为READY
|
||||
if job_info.always_free:
|
||||
job_info.status = JobStatus.READY
|
||||
job_info.update_timestamp()
|
||||
job_info.set_ready_timeout(10)
|
||||
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
||||
logger.trace(f"[DeviceActionManager] Job {job_log} always_free, start immediately")
|
||||
return True
|
||||
|
||||
# 检查是否有正在执行或准备执行的任务
|
||||
if device_key in self.active_jobs:
|
||||
# 有正在执行或准备执行的任务,加入队列
|
||||
@@ -176,11 +189,15 @@ class DeviceActionManager:
|
||||
logger.error(f"[DeviceActionManager] Job {job_log} is not in READY status, current: {job_info.status}")
|
||||
return False
|
||||
|
||||
# 检查设备上是否是这个job
|
||||
if device_key not in self.active_jobs or self.active_jobs[device_key].job_id != job_id:
|
||||
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
||||
logger.error(f"[DeviceActionManager] Job {job_log} is not the active job for {device_key}")
|
||||
return False
|
||||
# always_free的job不需要检查active_jobs
|
||||
if not job_info.always_free:
|
||||
# 检查设备上是否是这个job
|
||||
if device_key not in self.active_jobs or self.active_jobs[device_key].job_id != job_id:
|
||||
job_log = format_job_log(
|
||||
job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name
|
||||
)
|
||||
logger.error(f"[DeviceActionManager] Job {job_log} is not the active job for {device_key}")
|
||||
return False
|
||||
|
||||
# 开始执行任务,将状态从READY转换为STARTED
|
||||
job_info.status = JobStatus.STARTED
|
||||
@@ -203,6 +220,13 @@ class DeviceActionManager:
|
||||
job_info = self.all_jobs[job_id]
|
||||
device_key = job_info.device_action_key
|
||||
|
||||
# always_free的job直接清理,不影响队列
|
||||
if job_info.always_free:
|
||||
job_info.status = JobStatus.ENDED
|
||||
job_info.update_timestamp()
|
||||
del self.all_jobs[job_id]
|
||||
return None
|
||||
|
||||
# 移除活跃任务
|
||||
if device_key in self.active_jobs and self.active_jobs[device_key].job_id == job_id:
|
||||
del self.active_jobs[device_key]
|
||||
@@ -234,9 +258,14 @@ class DeviceActionManager:
|
||||
return None
|
||||
|
||||
def get_active_jobs(self) -> List[JobInfo]:
|
||||
"""获取所有正在执行的任务"""
|
||||
"""获取所有正在执行的任务(含active_jobs和always_free的STARTED job)"""
|
||||
with self.lock:
|
||||
return list(self.active_jobs.values())
|
||||
jobs = list(self.active_jobs.values())
|
||||
# 补充 always_free 的 STARTED job(它们不在 active_jobs 中)
|
||||
for job in self.all_jobs.values():
|
||||
if job.always_free and job.status == JobStatus.STARTED and job not in jobs:
|
||||
jobs.append(job)
|
||||
return jobs
|
||||
|
||||
def get_queued_jobs(self) -> List[JobInfo]:
|
||||
"""获取所有排队中的任务"""
|
||||
@@ -261,6 +290,14 @@ class DeviceActionManager:
|
||||
job_info = self.all_jobs[job_id]
|
||||
device_key = job_info.device_action_key
|
||||
|
||||
# always_free的job直接清理
|
||||
if job_info.always_free:
|
||||
job_info.status = JobStatus.ENDED
|
||||
del self.all_jobs[job_id]
|
||||
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
||||
logger.trace(f"[DeviceActionManager] Always-free job {job_log} cancelled")
|
||||
return True
|
||||
|
||||
# 如果是正在执行的任务
|
||||
if device_key in self.active_jobs and self.active_jobs[device_key].job_id == job_id:
|
||||
# 清理active job状态
|
||||
@@ -334,13 +371,18 @@ class DeviceActionManager:
|
||||
timeout_jobs = []
|
||||
|
||||
with self.lock:
|
||||
# 统计READY状态的任务数量
|
||||
ready_jobs_count = sum(1 for job in self.active_jobs.values() if job.status == JobStatus.READY)
|
||||
# 收集所有需要检查的 READY 任务(active_jobs + always_free READY jobs)
|
||||
ready_candidates = list(self.active_jobs.values())
|
||||
for job in self.all_jobs.values():
|
||||
if job.always_free and job.status == JobStatus.READY and job not in ready_candidates:
|
||||
ready_candidates.append(job)
|
||||
|
||||
ready_jobs_count = sum(1 for job in ready_candidates if job.status == JobStatus.READY)
|
||||
if ready_jobs_count > 0:
|
||||
logger.trace(f"[DeviceActionManager] Checking {ready_jobs_count} READY jobs for timeout") # type: ignore # noqa: E501
|
||||
|
||||
# 找到所有超时的READY任务(只检测,不处理)
|
||||
for job_info in self.active_jobs.values():
|
||||
for job_info in ready_candidates:
|
||||
if job_info.is_ready_timeout():
|
||||
timeout_jobs.append(job_info)
|
||||
job_log = format_job_log(
|
||||
@@ -369,6 +411,7 @@ class MessageProcessor:
|
||||
# 线程控制
|
||||
self.is_running = False
|
||||
self.thread = None
|
||||
self._loop = None # asyncio event loop引用,用于外部关闭websocket
|
||||
self.reconnect_count = 0
|
||||
|
||||
logger.info(f"[MessageProcessor] Initialized for URL: {websocket_url}")
|
||||
@@ -395,22 +438,31 @@ class MessageProcessor:
|
||||
def stop(self) -> None:
|
||||
"""停止消息处理线程"""
|
||||
self.is_running = False
|
||||
# 主动关闭websocket以快速中断消息接收循环
|
||||
ws = self.websocket
|
||||
loop = self._loop
|
||||
if ws and loop and loop.is_running():
|
||||
try:
|
||||
asyncio.run_coroutine_threadsafe(ws.close(), loop)
|
||||
except Exception:
|
||||
pass
|
||||
if self.thread and self.thread.is_alive():
|
||||
self.thread.join(timeout=2)
|
||||
logger.info("[MessageProcessor] Stopped")
|
||||
|
||||
def _run(self):
|
||||
"""运行消息处理主循环"""
|
||||
loop = asyncio.new_event_loop()
|
||||
self._loop = asyncio.new_event_loop()
|
||||
try:
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.run_until_complete(self._connection_handler())
|
||||
asyncio.set_event_loop(self._loop)
|
||||
self._loop.run_until_complete(self._connection_handler())
|
||||
except Exception as e:
|
||||
logger.error(f"[MessageProcessor] Thread error: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
finally:
|
||||
if loop:
|
||||
loop.close()
|
||||
if self._loop:
|
||||
self._loop.close()
|
||||
self._loop = None
|
||||
|
||||
async def _connection_handler(self):
|
||||
"""处理WebSocket连接和重连逻辑"""
|
||||
@@ -427,8 +479,10 @@ class MessageProcessor:
|
||||
async with websockets.connect(
|
||||
self.websocket_url,
|
||||
ssl=ssl_context,
|
||||
open_timeout=20,
|
||||
ping_interval=WSConfig.ping_interval,
|
||||
ping_timeout=10,
|
||||
close_timeout=5,
|
||||
additional_headers={
|
||||
"Authorization": f"Lab {BasicConfig.auth_secret()}",
|
||||
"EdgeSession": f"{self.session_id}",
|
||||
@@ -439,81 +493,97 @@ class MessageProcessor:
|
||||
self.connected = True
|
||||
self.reconnect_count = 0
|
||||
|
||||
logger.trace(f"[MessageProcessor] Connected to {self.websocket_url}")
|
||||
logger.info(f"[MessageProcessor] 已连接到 {self.websocket_url}")
|
||||
|
||||
# 启动发送协程
|
||||
send_task = asyncio.create_task(self._send_handler())
|
||||
send_task = asyncio.create_task(self._send_handler(), name="websocket-send_task")
|
||||
|
||||
# 每次连接(含重连)后重新向服务端注册,
|
||||
# 否则服务端不知道客户端已上线,不会推送消息。
|
||||
if self.websocket_client:
|
||||
self.websocket_client.publish_host_ready()
|
||||
|
||||
try:
|
||||
# 接收消息循环
|
||||
await self._message_handler()
|
||||
finally:
|
||||
# 必须在 async with __aexit__ 之前停止 send_task,
|
||||
# 否则 send_task 会在关闭握手期间继续发送数据,
|
||||
# 干扰 websockets 库的内部清理,导致 task 泄漏。
|
||||
self.connected = False
|
||||
send_task.cancel()
|
||||
try:
|
||||
await send_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
self.connected = False
|
||||
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
logger.warning("[MessageProcessor] Connection closed")
|
||||
self.connected = False
|
||||
logger.warning("[MessageProcessor] 与服务端连接中断")
|
||||
except TimeoutError:
|
||||
logger.warning(
|
||||
f"[MessageProcessor] 与服务端连接通信超时 (已尝试 {self.reconnect_count + 1} 次),请检查您的网络状况"
|
||||
)
|
||||
except websockets.exceptions.InvalidStatus as e:
|
||||
logger.warning(
|
||||
f"[MessageProcessor] 收到服务端注册码 {e.response.status_code}, 上一进程可能还未退出"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[MessageProcessor] Connection error: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
self.connected = False
|
||||
logger.error(f"[MessageProcessor] 尝试重连时出错 {str(e)}")
|
||||
finally:
|
||||
self.connected = False
|
||||
self.websocket = None
|
||||
|
||||
# 重连逻辑
|
||||
if self.is_running and self.reconnect_count < WSConfig.max_reconnect_attempts:
|
||||
if not self.is_running:
|
||||
break
|
||||
if self.reconnect_count < WSConfig.max_reconnect_attempts:
|
||||
self.reconnect_count += 1
|
||||
backoff = WSConfig.reconnect_interval
|
||||
logger.info(
|
||||
f"[MessageProcessor] Reconnecting in {WSConfig.reconnect_interval}s "
|
||||
f"(attempt {self.reconnect_count}/{WSConfig.max_reconnect_attempts})"
|
||||
"[MessageProcessor] 即将在 %s 秒后重连 (已尝试 %s/%s)",
|
||||
backoff,
|
||||
self.reconnect_count,
|
||||
WSConfig.max_reconnect_attempts,
|
||||
)
|
||||
await asyncio.sleep(WSConfig.reconnect_interval)
|
||||
elif self.reconnect_count >= WSConfig.max_reconnect_attempts:
|
||||
await asyncio.sleep(backoff)
|
||||
else:
|
||||
logger.error("[MessageProcessor] Max reconnection attempts reached")
|
||||
break
|
||||
else:
|
||||
self.reconnect_count -= 1
|
||||
|
||||
async def _message_handler(self):
|
||||
"""处理接收到的消息"""
|
||||
"""处理接收到的消息。
|
||||
|
||||
ConnectionClosed 不在此处捕获,让其向上传播到 _connection_handler,
|
||||
以便 async with websockets.connect() 的 __aexit__ 能感知连接已断,
|
||||
正确清理内部 task,避免 task 泄漏。
|
||||
"""
|
||||
if not self.websocket:
|
||||
logger.error("[MessageProcessor] WebSocket connection is None")
|
||||
return
|
||||
|
||||
try:
|
||||
async for message in self.websocket:
|
||||
try:
|
||||
data = json.loads(message)
|
||||
message_type = data.get("action", "")
|
||||
message_data = data.get("data")
|
||||
if self.session_id and self.session_id == data.get("edge_session"):
|
||||
await self._process_message(message_type, message_data)
|
||||
async for message in self.websocket:
|
||||
try:
|
||||
data = json.loads(message)
|
||||
message_type = data.get("action", "")
|
||||
message_data = data.get("data")
|
||||
if self.session_id and self.session_id == data.get("edge_session"):
|
||||
await self._process_message(message_type, message_data)
|
||||
else:
|
||||
if message_type.endswith("_material"):
|
||||
logger.trace(
|
||||
f"[MessageProcessor] 收到一条归属 {data.get('edge_session')} 的旧消息:{data}"
|
||||
)
|
||||
logger.debug(
|
||||
f"[MessageProcessor] 跳过了一条归属 {data.get('edge_session')} 的旧消息: {data.get('action')}"
|
||||
)
|
||||
else:
|
||||
if message_type.endswith("_material"):
|
||||
logger.trace(
|
||||
f"[MessageProcessor] 收到一条归属 {data.get('edge_session')} 的旧消息:{data}"
|
||||
)
|
||||
logger.debug(
|
||||
f"[MessageProcessor] 跳过了一条归属 {data.get('edge_session')} 的旧消息: {data.get('action')}"
|
||||
)
|
||||
else:
|
||||
await self._process_message(message_type, message_data)
|
||||
except json.JSONDecodeError:
|
||||
logger.error(f"[MessageProcessor] Invalid JSON received: {message}")
|
||||
except Exception as e:
|
||||
logger.error(f"[MessageProcessor] Error processing message: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
logger.info("[MessageProcessor] Message handler stopped - connection closed")
|
||||
except Exception as e:
|
||||
logger.error(f"[MessageProcessor] Message handler error: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
await self._process_message(message_type, message_data)
|
||||
except json.JSONDecodeError:
|
||||
logger.error(f"[MessageProcessor] Invalid JSON received: {message}")
|
||||
except Exception as e:
|
||||
logger.error(f"[MessageProcessor] Error processing message: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
async def _send_handler(self):
|
||||
"""处理发送队列中的消息"""
|
||||
@@ -562,6 +632,7 @@ class MessageProcessor:
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.debug("[MessageProcessor] Send handler cancelled")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"[MessageProcessor] Fatal error in send handler: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
@@ -593,6 +664,10 @@ class MessageProcessor:
|
||||
# elif message_type == "session_id":
|
||||
# self.session_id = message_data.get("session_id")
|
||||
# logger.info(f"[MessageProcessor] Session ID: {self.session_id}")
|
||||
elif message_type == "add_device":
|
||||
await self._handle_device_manage(message_data, "add")
|
||||
elif message_type == "remove_device":
|
||||
await self._handle_device_manage(message_data, "remove")
|
||||
elif message_type == "request_restart":
|
||||
await self._handle_request_restart(message_data)
|
||||
else:
|
||||
@@ -608,6 +683,24 @@ class MessageProcessor:
|
||||
if host_node:
|
||||
host_node.handle_pong_response(pong_data)
|
||||
|
||||
def _check_action_always_free(self, device_id: str, action_name: str) -> bool:
|
||||
"""检查该action是否标记为always_free,通过HostNode统一的_action_value_mappings查找"""
|
||||
try:
|
||||
host_node = HostNode.get_instance(0)
|
||||
if not host_node:
|
||||
return False
|
||||
# noinspection PyProtectedMember
|
||||
action_mappings = host_node._action_value_mappings.get(device_id)
|
||||
if not action_mappings:
|
||||
return False
|
||||
# 尝试直接匹配或 auto- 前缀匹配
|
||||
for key in [action_name, f"auto-{action_name}"]:
|
||||
if key in action_mappings:
|
||||
return action_mappings[key].get("always_free", False)
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
async def _handle_query_action_state(self, data: Dict[str, Any]):
|
||||
"""处理query_action_state消息"""
|
||||
device_id = data.get("device_id", "")
|
||||
@@ -615,6 +708,7 @@ class MessageProcessor:
|
||||
action_name = data.get("action_name", "")
|
||||
task_id = data.get("task_id", "")
|
||||
job_id = data.get("job_id", "")
|
||||
notebook_id = data.get("notebook_id", "")
|
||||
|
||||
if not all([device_id, action_name, task_id, job_id]):
|
||||
logger.error("[MessageProcessor] Missing required fields in query_action_state")
|
||||
@@ -622,15 +716,20 @@ class MessageProcessor:
|
||||
|
||||
device_action_key = f"/devices/{device_id}/{action_name}"
|
||||
|
||||
# 检查action是否为always_free
|
||||
action_always_free = self._check_action_always_free(device_id, action_name)
|
||||
|
||||
# 创建任务信息
|
||||
job_info = JobInfo(
|
||||
job_id=job_id,
|
||||
task_id=task_id,
|
||||
device_id=device_id,
|
||||
notebook_id=notebook_id,
|
||||
action_name=action_name,
|
||||
device_action_key=device_action_key,
|
||||
status=JobStatus.QUEUE,
|
||||
start_time=time.time(),
|
||||
always_free=action_always_free,
|
||||
)
|
||||
|
||||
# 添加到设备管理器
|
||||
@@ -640,13 +739,27 @@ class MessageProcessor:
|
||||
if can_start_immediately:
|
||||
# 可以立即开始
|
||||
await self._send_action_state_response(
|
||||
device_id, action_name, task_id, job_id, "query_action_status", True, 0
|
||||
device_id,
|
||||
action_name,
|
||||
task_id,
|
||||
job_id,
|
||||
"query_action_status",
|
||||
True,
|
||||
0,
|
||||
notebook_id=notebook_id,
|
||||
)
|
||||
logger.trace(f"[MessageProcessor] Job {job_log} can start immediately")
|
||||
else:
|
||||
# 需要排队
|
||||
await self._send_action_state_response(
|
||||
device_id, action_name, task_id, job_id, "query_action_status", False, 10
|
||||
device_id,
|
||||
action_name,
|
||||
task_id,
|
||||
job_id,
|
||||
"query_action_status",
|
||||
False,
|
||||
10,
|
||||
notebook_id=notebook_id,
|
||||
)
|
||||
logger.trace(f"[MessageProcessor] Job {job_log} queued")
|
||||
|
||||
@@ -657,9 +770,43 @@ class MessageProcessor:
|
||||
async def _handle_job_start(self, data: Dict[str, Any]):
|
||||
"""处理job_start消息"""
|
||||
try:
|
||||
if not data.get("sample_material"):
|
||||
data["sample_material"] = {}
|
||||
req = JobAddReq(**data)
|
||||
|
||||
job_log = format_job_log(req.job_id, req.task_id, req.device_id, req.action)
|
||||
|
||||
# 服务端对always_free动作可能跳过query_action_state直接发job_start,
|
||||
# 此时job尚未注册,需要自动补注册
|
||||
existing_job = self.device_manager.get_job_info(req.job_id)
|
||||
if not existing_job:
|
||||
action_name = req.action
|
||||
device_action_key = f"/devices/{req.device_id}/{action_name}"
|
||||
action_always_free = self._check_action_always_free(req.device_id, action_name)
|
||||
|
||||
if action_always_free:
|
||||
job_info = JobInfo(
|
||||
job_id=req.job_id,
|
||||
task_id=req.task_id,
|
||||
device_id=req.device_id,
|
||||
notebook_id=req.notebook_id,
|
||||
action_name=action_name,
|
||||
device_action_key=device_action_key,
|
||||
status=JobStatus.QUEUE,
|
||||
start_time=time.time(),
|
||||
always_free=True,
|
||||
)
|
||||
self.device_manager.add_queue_request(job_info)
|
||||
existing_job = job_info
|
||||
logger.info(f"[MessageProcessor] Job {job_log} always_free, auto-registered from direct job_start")
|
||||
else:
|
||||
logger.error(f"[MessageProcessor] Job {job_log} not registered (missing query_action_state)")
|
||||
return
|
||||
|
||||
if existing_job and req.notebook_id and not existing_job.notebook_id:
|
||||
existing_job.notebook_id = req.notebook_id
|
||||
notebook_id = req.notebook_id or (existing_job.notebook_id if existing_job else "")
|
||||
|
||||
success = self.device_manager.start_job(req.job_id)
|
||||
if not success:
|
||||
logger.error(f"[MessageProcessor] Failed to start job {job_log}")
|
||||
@@ -675,6 +822,7 @@ class MessageProcessor:
|
||||
action_name=req.action,
|
||||
task_id=req.task_id,
|
||||
job_id=req.job_id,
|
||||
notebook_id=notebook_id,
|
||||
device_action_key=device_action_key,
|
||||
)
|
||||
|
||||
@@ -714,6 +862,7 @@ class MessageProcessor:
|
||||
"job_id": req.job_id,
|
||||
"task_id": req.task_id,
|
||||
"device_id": req.device_id,
|
||||
"notebook_id": queue_item.notebook_id,
|
||||
"action_name": req.action,
|
||||
"status": "failed",
|
||||
"feedback_data": {},
|
||||
@@ -735,6 +884,7 @@ class MessageProcessor:
|
||||
"query_action_status",
|
||||
True,
|
||||
0,
|
||||
notebook_id=next_job.notebook_id,
|
||||
)
|
||||
next_job_log = format_job_log(
|
||||
next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name
|
||||
@@ -905,6 +1055,37 @@ class MessageProcessor:
|
||||
)
|
||||
thread.start()
|
||||
|
||||
async def _handle_device_manage(self, device_list: list[ResourceDictType], action: str):
|
||||
"""Handle add_device / remove_device from LabGo server."""
|
||||
if not device_list:
|
||||
return
|
||||
|
||||
for item in device_list:
|
||||
target_node_id = item.get("target_node_id", "host_node")
|
||||
|
||||
def _notify(target_id: str, act: str, cfg: ResourceDictType):
|
||||
try:
|
||||
host_node = HostNode.get_instance(timeout=5)
|
||||
if not host_node:
|
||||
logger.error(f"[DeviceManage] HostNode not available for {act}_device")
|
||||
return
|
||||
success = host_node.notify_device_manage(target_id, act, cfg)
|
||||
if success:
|
||||
logger.info(f"[DeviceManage] {act}_device completed on {target_id}")
|
||||
else:
|
||||
logger.warning(f"[DeviceManage] {act}_device failed on {target_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"[DeviceManage] Error in {act}_device: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
thread = threading.Thread(
|
||||
target=_notify,
|
||||
args=(target_node_id, action, item),
|
||||
daemon=True,
|
||||
name=f"DeviceManage-{action}-{item.get('id', '')}",
|
||||
)
|
||||
thread.start()
|
||||
|
||||
async def _handle_request_restart(self, data: Dict[str, Any]):
|
||||
"""
|
||||
处理重启请求
|
||||
@@ -916,10 +1097,9 @@ class MessageProcessor:
|
||||
logger.info(f"[MessageProcessor] Received restart request, reason: {reason}, delay: {delay}s")
|
||||
|
||||
# 发送确认消息
|
||||
if self.websocket_client:
|
||||
await self.websocket_client.send_message(
|
||||
{"action": "restart_acknowledged", "data": {"reason": reason, "delay": delay}}
|
||||
)
|
||||
self.send_message(
|
||||
{"action": "restart_acknowledged", "data": {"reason": reason, "delay": delay}}
|
||||
)
|
||||
|
||||
# 设置全局重启标志
|
||||
import unilabos.app.main as main_module
|
||||
@@ -951,7 +1131,15 @@ class MessageProcessor:
|
||||
logger.info(f"[MessageProcessor] Restart cleanup scheduled")
|
||||
|
||||
async def _send_action_state_response(
|
||||
self, device_id: str, action_name: str, task_id: str, job_id: str, typ: str, free: bool, need_more: int
|
||||
self,
|
||||
device_id: str,
|
||||
action_name: str,
|
||||
task_id: str,
|
||||
job_id: str,
|
||||
typ: str,
|
||||
free: bool,
|
||||
need_more: int,
|
||||
notebook_id: str = "",
|
||||
):
|
||||
"""发送动作状态响应"""
|
||||
message = {
|
||||
@@ -962,8 +1150,9 @@ class MessageProcessor:
|
||||
"action_name": action_name,
|
||||
"task_id": task_id,
|
||||
"job_id": job_id,
|
||||
"notebook_id": notebook_id,
|
||||
"free": free,
|
||||
"need_more": need_more,
|
||||
"need_more": need_more + 1,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1021,6 +1210,7 @@ class QueueProcessor:
|
||||
def stop(self) -> None:
|
||||
"""停止队列处理线程"""
|
||||
self.is_running = False
|
||||
self.queue_update_event.set() # 立即唤醒等待中的线程
|
||||
if self.thread and self.thread.is_alive():
|
||||
self.thread.join(timeout=2)
|
||||
logger.info("[QueueProcessor] Stopped")
|
||||
@@ -1043,6 +1233,7 @@ class QueueProcessor:
|
||||
action_name=timeout_job.action_name,
|
||||
task_id=timeout_job.task_id,
|
||||
job_id=timeout_job.job_id,
|
||||
notebook_id=timeout_job.notebook_id,
|
||||
device_action_key=timeout_job.device_action_key,
|
||||
)
|
||||
# 发布超时失败状态,这会触发正常的job完成流程
|
||||
@@ -1101,8 +1292,9 @@ class QueueProcessor:
|
||||
"action_name": job_info.action_name,
|
||||
"task_id": job_info.task_id,
|
||||
"job_id": job_info.job_id,
|
||||
"notebook_id": job_info.notebook_id,
|
||||
"free": False,
|
||||
"need_more": 10,
|
||||
"need_more": 10 + 1,
|
||||
},
|
||||
}
|
||||
self.message_processor.send_message(message)
|
||||
@@ -1118,9 +1310,20 @@ class QueueProcessor:
|
||||
if not queued_jobs:
|
||||
return
|
||||
|
||||
logger.debug(f"[QueueProcessor] Sending busy status for {len(queued_jobs)} queued jobs")
|
||||
queue_summary = {}
|
||||
for j in queued_jobs:
|
||||
key = f"{j.device_id}/{j.action_name}"
|
||||
queue_summary[key] = queue_summary.get(key, 0) + 1
|
||||
logger.debug(
|
||||
f"[QueueProcessor] Sending busy status for {len(queued_jobs)} queued jobs: {queue_summary}"
|
||||
)
|
||||
|
||||
for job_info in queued_jobs:
|
||||
# 快照可能已过期:在遍历过程中 end_job() 可能已将此 job 移至 READY,
|
||||
# 此时不应再发送 busy/need_more,否则会覆盖已发出的 free=True 通知
|
||||
if job_info.status != JobStatus.QUEUE:
|
||||
continue
|
||||
|
||||
message = {
|
||||
"action": "report_action_state",
|
||||
"data": {
|
||||
@@ -1129,8 +1332,9 @@ class QueueProcessor:
|
||||
"action_name": job_info.action_name,
|
||||
"task_id": job_info.task_id,
|
||||
"job_id": job_info.job_id,
|
||||
"notebook_id": job_info.notebook_id,
|
||||
"free": False,
|
||||
"need_more": 10,
|
||||
"need_more": 10 + 1,
|
||||
},
|
||||
}
|
||||
success = self.message_processor.send_message(message)
|
||||
@@ -1174,12 +1378,15 @@ class QueueProcessor:
|
||||
"action_name": next_job.action_name,
|
||||
"task_id": next_job.task_id,
|
||||
"job_id": next_job.job_id,
|
||||
"notebook_id": next_job.notebook_id,
|
||||
"free": True,
|
||||
"need_more": 0,
|
||||
},
|
||||
}
|
||||
self.message_processor.send_message(message)
|
||||
# next_job_log = format_job_log(next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name)
|
||||
# next_job_log = format_job_log(
|
||||
# next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name
|
||||
# )
|
||||
# logger.debug(f"[QueueProcessor] Notified next job {next_job_log} can start")
|
||||
|
||||
# 立即触发下一轮状态检查
|
||||
@@ -1213,6 +1420,10 @@ class WebSocketClient(BaseCommunicationClient):
|
||||
self.message_processor = MessageProcessor(self.websocket_url, self.send_queue, self.device_manager)
|
||||
self.queue_processor = QueueProcessor(self.device_manager, self.message_processor)
|
||||
|
||||
# running状态debounce缓存: {job_id: (last_send_timestamp, last_feedback_data)}
|
||||
self._job_running_last_sent: Dict[str, tuple] = {}
|
||||
self._job_running_debounce_interval: float = 10.0 # 秒
|
||||
|
||||
# 设置相互引用
|
||||
self.message_processor.set_queue_processor(self.queue_processor)
|
||||
self.message_processor.set_websocket_client(self)
|
||||
@@ -1269,8 +1480,8 @@ class WebSocketClient(BaseCommunicationClient):
|
||||
message = {"action": "normal_exit", "data": {"session_id": session_id}}
|
||||
self.message_processor.send_message(message)
|
||||
logger.info(f"[WebSocketClient] Sent normal_exit message with session_id: {session_id}")
|
||||
# 给一点时间让消息发送出去
|
||||
time.sleep(1)
|
||||
# send_handler 每100ms检查一次队列,等300ms足以让消息发出
|
||||
time.sleep(0.3)
|
||||
except Exception as e:
|
||||
logger.warning(f"[WebSocketClient] Failed to send normal_exit message: {str(e)}")
|
||||
|
||||
@@ -1312,28 +1523,39 @@ class WebSocketClient(BaseCommunicationClient):
|
||||
logger.debug(f"[WebSocketClient] Not connected, cannot publish job status for job_id: {item.job_id}")
|
||||
return
|
||||
|
||||
job_log = format_job_log(item.job_id, item.task_id, item.device_id, item.action_name)
|
||||
|
||||
# 拦截最终结果状态,与原版本逻辑一致
|
||||
if status in ["success", "failed"]:
|
||||
self._job_running_last_sent.pop(item.job_id, None)
|
||||
|
||||
host_node = HostNode.get_instance(0)
|
||||
if host_node:
|
||||
# 从HostNode的device_action_status中移除job_id
|
||||
try:
|
||||
host_node._device_action_status[item.device_action_key].job_ids.pop(item.job_id, None)
|
||||
except (KeyError, AttributeError):
|
||||
logger.warning(f"[WebSocketClient] Failed to remove job {item.job_id} from HostNode status")
|
||||
|
||||
# logger.debug(f"[WebSocketClient] Intercepting final status for job_id: {item.job_id} - {status}")
|
||||
|
||||
# 通知队列处理器job完成(包括timeout的job)
|
||||
self.queue_processor.handle_job_completed(item.job_id, status)
|
||||
|
||||
# 发送job状态消息
|
||||
# running状态按job_id做debounce,内容变化时仍然上报
|
||||
if status == "running":
|
||||
now = time.time()
|
||||
cached = self._job_running_last_sent.get(item.job_id)
|
||||
if cached is not None:
|
||||
last_ts, last_data = cached
|
||||
if now - last_ts < self._job_running_debounce_interval and last_data == feedback_data:
|
||||
logger.trace(f"[WebSocketClient] Job status debounced (skip): {job_log} - {status}")
|
||||
return
|
||||
self._job_running_last_sent[item.job_id] = (now, feedback_data)
|
||||
|
||||
message = {
|
||||
"action": "job_status",
|
||||
"data": {
|
||||
"job_id": item.job_id,
|
||||
"task_id": item.task_id,
|
||||
"device_id": item.device_id,
|
||||
"notebook_id": item.notebook_id,
|
||||
"action_name": item.action_name,
|
||||
"status": status,
|
||||
"feedback_data": feedback_data,
|
||||
@@ -1343,7 +1565,6 @@ class WebSocketClient(BaseCommunicationClient):
|
||||
}
|
||||
self.message_processor.send_message(message)
|
||||
|
||||
job_log = format_job_log(item.job_id, item.task_id, item.device_id, item.action_name)
|
||||
logger.trace(f"[WebSocketClient] Job status published: {job_log} - {status}")
|
||||
|
||||
def send_ping(self, ping_id: str, timestamp: float) -> None:
|
||||
|
||||
@@ -95,8 +95,29 @@ def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float:
|
||||
return total_volume
|
||||
|
||||
|
||||
def is_integrated_pump(node_name):
|
||||
return "pump" in node_name and "valve" in node_name
|
||||
def is_integrated_pump(node_class: str, node_name: str = "") -> bool:
|
||||
"""
|
||||
判断是否为泵阀一体设备
|
||||
"""
|
||||
class_lower = (node_class or "").lower()
|
||||
name_lower = (node_name or "").lower()
|
||||
|
||||
if "pump" not in class_lower and "pump" not in name_lower:
|
||||
return False
|
||||
|
||||
integrated_markers = [
|
||||
"valve",
|
||||
"pump_valve",
|
||||
"pumpvalve",
|
||||
"integrated",
|
||||
"transfer_pump",
|
||||
]
|
||||
|
||||
for marker in integrated_markers:
|
||||
if marker in class_lower or marker in name_lower:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def find_connected_pump(G, valve_node):
|
||||
@@ -186,7 +207,9 @@ def build_pump_valve_maps(G, pump_backbone):
|
||||
debug_print(f"🔧 过滤后的骨架: {filtered_backbone}")
|
||||
|
||||
for node in filtered_backbone:
|
||||
if is_integrated_pump(G.nodes[node]["class"]):
|
||||
node_data = G.nodes.get(node, {})
|
||||
node_class = node_data.get("class", "") or ""
|
||||
if is_integrated_pump(node_class, node):
|
||||
pumps_from_node[node] = node
|
||||
valve_from_node[node] = node
|
||||
debug_print(f" - 集成泵-阀: {node}")
|
||||
|
||||
@@ -23,6 +23,8 @@ class BasicConfig:
|
||||
disable_browser = False # 禁止浏览器自动打开
|
||||
port = 8002 # 本地HTTP服务
|
||||
check_mode = False # CI 检查模式,用于验证 registry 导入和文件一致性
|
||||
test_mode = False # 测试模式,所有动作不实际执行,返回模拟结果
|
||||
extra_resource = False # 是否加载lab_开头的额外资源
|
||||
# 'TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'
|
||||
log_level: Literal["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "DEBUG"
|
||||
|
||||
@@ -39,12 +41,12 @@ class BasicConfig:
|
||||
class WSConfig:
|
||||
reconnect_interval = 5 # 重连间隔(秒)
|
||||
max_reconnect_attempts = 999 # 最大重连次数
|
||||
ping_interval = 30 # ping间隔(秒)
|
||||
ping_interval = 20 # ping间隔(秒)
|
||||
|
||||
|
||||
# HTTP配置
|
||||
class HTTPConfig:
|
||||
remote_addr = "https://uni-lab.bohrium.com/api/v1"
|
||||
remote_addr = "https://leap-lab.bohrium.com/api/v1"
|
||||
|
||||
|
||||
# ROS配置
|
||||
@@ -145,5 +147,5 @@ def load_config(config_path=None):
|
||||
traceback.print_exc()
|
||||
exit(1)
|
||||
else:
|
||||
config_path = os.path.join(os.path.dirname(__file__), "local_config.py")
|
||||
config_path = os.path.join(os.path.dirname(__file__), "example_config.py")
|
||||
load_config(config_path)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
from abc import abstractmethod
|
||||
from functools import wraps
|
||||
import inspect
|
||||
|
||||
78
unilabos/devices/donghua_ec/Donghua_EC_User_Guide.md
Normal file
78
unilabos/devices/donghua_ec/Donghua_EC_User_Guide.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Donghua EC 用户指南(UniLab 接入版)
|
||||
|
||||
## 概述
|
||||
|
||||
- 提供两套使用方式:
|
||||
- 测试封装动作:一条指令完成“启动实验 → 实时采样写文件 → 导出数据”,可选自动停止。
|
||||
- 基础启动动作:按需组合“启动实验、实时输出、停止、导出”,更灵活可编排。
|
||||
|
||||
## 设备配置
|
||||
|
||||
- `interface_dir`:DHInterface 目录(包含 `ECCore.dll` 与配置文件),示例:`d:\Uni-Lab-OS\Uni-Lab-OS\unilabos\devices\donghua_ec\x64release\DHInterface`(注册见 `unilabos/registry/devices/donghua_ec.yaml:1940`)。
|
||||
- `dll_path`(可选):若与 `interface_dir` 不一致,可直接指定 `ECCore.dll` 完整路径(`donghua_ec.yaml:1936`)。
|
||||
- 默认通道:`machine_id`(`donghua_ec.yaml:1944`)。
|
||||
|
||||
## 初始化
|
||||
|
||||
- 后端自动:设备注册后会自动调用 `auto-initialize`(加载 DLL)与 `auto-post_init`(注入),无需前端干预(`donghua_ec.yaml:27`、`donghua_ec.yaml:48`)。
|
||||
- 手动(可选):
|
||||
- 调用 `auto-initialize`:`{"device_id":"<设备ID>","action":"auto-initialize"}`
|
||||
- 调用 `auto-post_init`:`{"device_id":"<设备ID>","action":"auto-post_init"}`
|
||||
|
||||
## 动作总览
|
||||
|
||||
- 测试封装动作(均要求传入 `output_dir`):
|
||||
- `test_open_circuit_energy`(默认 `stop_after=true`,使用轮询检测实验结束后再停止与导出,不再使用 `wait_seconds`)
|
||||
- `test_eis`(默认 `stop_after=false`,避免提前结束,`donghua_ec.yaml:1480`)
|
||||
- `test_gitt`(默认 `stop_after=false`,`donghua_ec.yaml:1627`)
|
||||
- `test_linear_scan_voltammetry`(默认 `stop_after=false`,必填 `output_dir`,参考 `donghua_ec.yaml:1750` 及后续)
|
||||
- 基础启动与组合:
|
||||
- `start_open_circuit_energy`、`start_eis`、`start_gitt`、`start_linear_scan_voltammetry`
|
||||
- 实时输出:`start_realtime_output` / `stop_realtime_output`(`donghua_ec.yaml:1068`、`donghua_ec.yaml:1155`)
|
||||
- 停止实验:`stop_experiment`(`donghua_ec.yaml:1118`)
|
||||
- 导出数据:`export_*_data`(如 `export_eis_data`、`export_gitt_data` 等,均要求 `output_dir/dest_dir`)
|
||||
|
||||
## 快速测试流程(推荐)
|
||||
|
||||
- 开路电位:
|
||||
- 请求:
|
||||
```json
|
||||
{"device_id":"<设备ID>","action":"test_open_circuit_energy","action_args":{"output_dir":"d:/data/oc","interval":0.5,"stop_after":true}}
|
||||
```
|
||||
- 返回包含:`success`、`realtime_file`、`export_files`、`export_dest`。
|
||||
- 阻抗(EIS):
|
||||
- 请求(只需给导出目录,其他用默认即可):
|
||||
```json
|
||||
{"device_id":"<设备ID>","action":"test_eis","action_args":{"output_dir":"d:/data/eis","start_freq":10000,"end_freq":0.1,"amplitude":0.01,"point_count":10,"interval":0.5}}
|
||||
```
|
||||
- 默认不自动停止(`stop_after=false`),可在完成采样后继续扫频;若需自动停,传 `stop_after=true`。
|
||||
- GITT:
|
||||
- 请求:
|
||||
```json
|
||||
{"device_id":"<设备ID>","action":"test_gitt","action_args":{"output_dir":"d:/data/gitt","current":1.0,"time_per_point_cc":0.1,"continue_time_cc":60,"time_per_point_oc":0.1,"continue_time_oc":60,"is_voltage_trig":true,"voltage_or_current_trig_direction":0,"voltage_or_current_trig_value":0,"interval":0.5}}
|
||||
```
|
||||
|
||||
## 基础启动与组合(灵活编排)
|
||||
|
||||
- 启动 EIS:
|
||||
- `{"device_id":"<设备ID>","action":"start_eis","action_args":{"start_freq":10000,"end_freq":0.1,"amplitude":0.01,"point_count":10}}`
|
||||
- 开启实时输出:
|
||||
- `{"device_id":"<设备ID>","action":"start_realtime_output","action_args":{"interval":0.5}}`
|
||||
- 关闭实时输出并获取文件:
|
||||
- `{"device_id":"<设备ID>","action":"stop_realtime_output"}`
|
||||
- 导出数据到目录:
|
||||
- `{"device_id":"<设备ID>","action":"export_eis_data","action_args":{"output_dir":"d:/data/eis"}}`
|
||||
- 停止实验(可选):
|
||||
- `{"device_id":"<设备ID>","action":"stop_experiment"}`
|
||||
|
||||
## 重要说明
|
||||
|
||||
- 必填导出目录:所有 `test_*` 和 `export_*` 动作需要提供 `output_dir`(或 `dest_dir`),否则不会复制数据到目标位置(`donghua_ec.yaml:1545`、`donghua_ec.yaml:1710`)。
|
||||
- 关于提前结束:非开路的测试封装动作默认 `stop_after=false`,避免在实时采样后调用 `stop_experiment`,从而导致频率扫描未达到 `end_freq` 就停止(修复见 `donghua_ec.yaml:1480`、`donghua_ec.yaml:1627`)。
|
||||
- 实时文件位置:若未指定 `dest_dir`,实时输出会写入 `interface_dir/SourceData/<日期>/<实验子目录>`(实现参考 `unilabos/devices/donghua_ec/donghua_ec.py:1042`)。
|
||||
|
||||
## 数据字段(参考)
|
||||
|
||||
- EIS 拆分:`time/zre/zim/z/freq/phase/edc`(实现参考 `unilabos/devices/donghua_ec/donghua_ec.py:1109`)。
|
||||
- 线性扫描与循环伏安:`time/potential/current` 等(实现参考 `donghua_ec.py:1111`、`donghua_ec.py:1114`)。
|
||||
- 开路电位:写入时间序列与电位(`donghua_ec.py:1045`)。
|
||||
3
unilabos/devices/donghua_ec/__init__.py
Normal file
3
unilabos/devices/donghua_ec/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .donghua_ec import DonghuaEC
|
||||
|
||||
__all__ = ["DonghuaEC"]
|
||||
24
unilabos/devices/donghua_ec/donghua_ec.json
Normal file
24
unilabos/devices/donghua_ec/donghua_ec.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "donghua_ec_device",
|
||||
"name": "Donghua_EC",
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "donghua_ec",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"interface_dir": "D:/Uni-Lab-OS/Uni-Lab-OS/unilabos/devices/donghua_ec/x64release/DHInterface",
|
||||
"dll_path": "",
|
||||
"machine_id": 0
|
||||
},
|
||||
"data": {},
|
||||
"children": []
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
2033
unilabos/devices/donghua_ec/donghua_ec.py
Normal file
2033
unilabos/devices/donghua_ec/donghua_ec.py
Normal file
File diff suppressed because it is too large
Load Diff
358
unilabos/devices/donghua_ec/exports/1号机().txt
Normal file
358
unilabos/devices/donghua_ec/exports/1号机().txt
Normal file
@@ -0,0 +1,358 @@
|
||||
Time(s) E(mV) I(mA) Q(mC) Capacity(mAh) Energy(Wh) P(W)
|
||||
0.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
Time(s) E(mV) I(mA) Q(mC) Capacity(mAh) Energy(Wh) P(W)
|
||||
0.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
398
unilabos/devices/donghua_ec/exports/2号机().txt
Normal file
398
unilabos/devices/donghua_ec/exports/2号机().txt
Normal file
@@ -0,0 +1,398 @@
|
||||
Time(s) E(mV) I(mA) Q(mC) Capacity(mAh) Energy(Wh) P(W)
|
||||
0.100000 1992.742065 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.200000 1992.826050 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.300000 1993.161743 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.400000 1993.245605 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.500000 1993.077881 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.600000 1992.154907 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.700000 1992.909912 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.800000 1993.245605 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.900000 1993.077881 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.000000 1993.161743 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.100000 1992.826050 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.200000 1993.245605 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.300000 1993.161743 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.400000 1992.742065 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.500000 1992.993896 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.600000 1992.742065 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.700000 1992.909912 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.800000 1993.245605 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.900000 1993.245605 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.000000 1993.245605 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.100000 1993.077881 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.200000 1993.245605 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.300000 1993.665161 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.400000 1993.665161 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.500000 1993.665161 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.600000 1993.916748 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.700000 1994.000732 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.800000 1993.413452 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.900000 1992.993896 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.000000 1993.665161 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.100000 1993.581177 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.200000 1993.665161 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.300000 1993.413452 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.400000 1993.497314 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.500000 1993.161743 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.600000 1993.916748 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.700000 1993.749023 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.800000 1994.000732 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.900000 1994.084717 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.000000 1993.749023 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.100000 1993.916748 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.200000 1993.497314 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.300000 1993.749023 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.400000 1993.665161 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.500000 1993.413452 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.600000 1993.749023 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.700000 1994.168579 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.800000 1993.916748 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.900000 1993.497314 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.000000 1993.665161 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.100000 1994.084717 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.200000 1994.084717 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.300000 1994.420288 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.400000 1994.504150 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.500000 1994.755859 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.600000 1994.168579 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.700000 1995.259399 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.800000 1995.175415 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.900000 1994.336426 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.000000 1995.511108 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.100000 1995.091431 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.200000 1994.671997 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.300000 1994.504150 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.400000 1994.923706 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.500000 1995.511108 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.600000 1995.427246 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.700000 1994.923706 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.800000 1995.678833 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.900000 1995.762817 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.000000 1995.511108 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.100000 1995.511108 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.200000 1995.259399 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.300000 1994.755859 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.400000 1994.671997 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.500000 1994.755859 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.600000 1995.175415 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.700000 1995.007568 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.800000 1995.091431 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.900000 1994.923706 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.000000 1995.007568 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.100000 1996.685791 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.200000 1995.762817 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.300000 1995.259399 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.400000 1994.420288 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.500000 1993.329590 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.600000 1993.077881 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.700000 1992.742065 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.800000 1993.245605 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.900000 1993.329590 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.000000 1993.665161 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.100000 1994.336426 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.200000 1995.091431 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.300000 1995.259399 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.400000 1995.175415 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.500000 1995.343262 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.600000 1995.175415 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.700000 1995.091431 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.800000 1995.427246 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.900000 1995.594971 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.000000 1995.091431 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.100000 1994.504150 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.200000 1994.336426 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.300000 1994.000732 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.400000 1993.665161 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.500000 1993.329590 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.600000 1994.000732 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.700000 1994.671997 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.800000 1994.755859 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.900000 1994.671997 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.000000 1994.755859 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.100000 1994.923706 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.200000 1995.091431 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.300000 1996.014404 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.400000 1995.091431 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.500000 1995.259399 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.600000 1995.007568 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.700000 1995.343262 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.800000 1996.014404 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.900000 1995.762817 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.000000 1995.678833 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.100000 1996.434082 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.200000 1996.098389 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.300000 1995.343262 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.400000 1995.511108 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.500000 1995.511108 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.600000 1995.511108 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.700000 1995.511108 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.800000 1995.678833 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.900000 1995.511108 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.000000 1995.259399 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.100000 1995.511108 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.200000 1995.007568 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.300000 1995.175415 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.400000 1995.259399 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.500000 1995.762817 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.600000 1995.930542 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.700000 1995.343262 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.800000 1995.175415 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.900000 1995.175415 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.000000 1995.678833 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.100000 1996.182251 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.200000 1996.434082 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.300000 1995.846680 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.400000 1996.350220 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.500000 1996.098389 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.600000 1995.343262 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.700000 1995.427246 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.800000 1995.427246 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.900000 1995.511108 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.000000 1995.678833 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.100000 1995.343262 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.200000 1995.007568 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.300000 1995.175415 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.400000 1995.427246 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.500000 1995.259399 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.600000 1995.175415 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.700000 1995.091431 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.800000 1994.671997 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.900000 1993.665161 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.000000 1994.084717 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.100000 1994.168579 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.200001 1994.252563 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.299999 1994.839722 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.400000 1996.350220 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.500000 1995.091431 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.600000 1994.588135 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.700001 1994.671997 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.799999 1995.259399 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.900000 1995.427246 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.000000 1995.427246 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.100000 1995.091431 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.200001 1994.839722 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.299999 1994.755859 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.400000 1995.007568 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.500000 1994.588135 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.600000 1994.755859 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.700001 1994.755859 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.799999 1994.420288 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.900000 1994.336426 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.000000 1994.420288 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.100000 1994.755859 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.200001 1994.671997 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.299999 1994.923706 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.400000 1995.007568 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.500000 1994.839722 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.600000 1993.832886 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.700001 1994.588135 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.799999 1994.839722 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.900000 1994.839722 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.000000 1994.671997 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.100000 1994.671997 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.200001 1994.588135 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.299999 1994.420288 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.400000 1994.336426 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.500000 1994.336426 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.600000 1994.671997 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.700001 1994.755859 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.799999 1995.007568 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
Time(s) E(mV) I(mA) Q(mC) Capacity(mAh) Energy(Wh) P(W)
|
||||
0.100000 2057.013672 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.200000 2057.684814 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.300000 2057.936523 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.400000 2058.020508 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.500000 2058.523682 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.600000 2058.775635 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.700000 2058.775635 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.800000 2058.859619 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.900000 2059.362793 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.000000 2059.279053 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.100000 2059.446777 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.200000 2060.034180 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.300000 2060.285889 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.400000 2060.201904 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.500000 2060.537598 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.600000 2060.789307 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.700000 2061.041016 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.800000 2061.125000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.900000 2061.628418 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.000000 2061.880127 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.100000 2061.963867 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.200000 2062.047852 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.300000 2062.383545 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.400000 2062.718994 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.500000 2062.718994 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.600000 2062.970703 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.700000 2063.306396 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.800000 2063.306396 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.900000 2063.558105 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.000000 2063.642090 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.100000 2064.061523 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.200000 2064.061523 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.300000 2064.229248 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.400000 2064.229248 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.500000 2064.732910 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.600000 2064.732910 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.700000 2065.236328 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.800000 2065.152344 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.900000 2065.404053 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.000000 2065.320068 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.100000 2065.739746 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.200000 2065.907471 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.300000 2065.991455 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.400000 2065.991455 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.500000 2065.991455 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.600000 2066.326904 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.700000 2066.830566 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.800000 2066.578613 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.900000 2066.998291 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.000000 2067.333984 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.100000 2067.585449 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.200000 2067.585449 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.300000 2067.669434 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.400000 2067.837402 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.500000 2067.921387 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.600000 2068.173096 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.700000 2068.340820 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.800000 2068.592529 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.900000 2068.760254 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.000000 2068.592529 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.100000 2068.760254 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.200000 2069.095947 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.300000 2069.095947 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.400000 2069.011963 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.500000 2069.683105 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.600000 2069.599365 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.700000 2069.767090 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.800000 2069.767090 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.900000 2069.851074 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.000000 2070.186768 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.100000 2069.851074 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.200000 2070.354492 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.300000 2070.186768 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.400000 2070.606201 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.500000 2070.773926 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.600000 2070.606201 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.700000 2071.109619 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.800000 2070.773926 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.900000 2071.109619 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.000000 2071.025635 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.100000 2071.361328 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.200000 2071.445312 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.300000 2071.445312 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.400000 2071.529297 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.500000 2071.864746 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.600000 2072.116455 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.700000 2071.948730 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.800000 2071.948730 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.900000 2072.284424 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.000000 2072.368164 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.100000 2072.452148 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.200000 2072.619873 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.300000 2072.955566 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.400000 2072.871582 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.500000 2072.955566 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.600000 2073.207275 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.700000 2073.207275 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.800000 2073.458984 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.900000 2073.291016 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.000000 2073.458984 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.100000 2073.458984 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.200000 2073.878418 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.300000 2073.794678 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.400000 2074.046387 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.500000 2074.046387 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.600000 2073.962402 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.700000 2074.214111 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.800000 2074.214111 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.900000 2074.130127 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.000000 2074.717529 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.100000 2074.549805 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.200000 2074.801514 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.300000 2074.885498 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.400000 2074.885498 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.500000 2074.801514 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.600000 2075.137207 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.700000 2075.137207 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.800000 2075.220947 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.900000 2075.304932 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.000000 2075.556641 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.100000 2075.388672 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.200000 2075.808350 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.300000 2075.808350 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.400000 2075.808350 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.500000 2075.892334 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.600000 2075.892334 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.700000 2075.976074 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.800000 2076.144043 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.900000 2076.060059 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.000000 2076.395752 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.100000 2076.395752 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.200000 2076.479492 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.300000 2076.563477 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.400000 2076.563477 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.500000 2076.731201 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.600000 2076.983154 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.700000 2077.150879 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.800000 2077.066895 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.900000 2077.402588 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.000000 2077.318604 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.100000 2077.402588 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.200000 2077.486328 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.300000 2077.570312 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.400000 2077.570312 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.500000 2078.073730 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.600000 2077.906006 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.700000 2077.822021 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.800000 2078.073730 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.900000 2078.157715 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.000000 2078.157715 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.100000 2078.241699 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.200000 2078.409424 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.300000 2078.409424 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.400000 2078.661133 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.500000 2078.828857 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.600000 2078.577148 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.700000 2078.912842 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.800000 2078.996582 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.900000 2078.996582 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.000000 2079.248535 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.100000 2079.332275 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.200001 2079.248535 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.299999 2079.500244 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.400000 2079.667969 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.500000 2079.835693 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.600000 2079.835693 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.700001 2079.583984 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.799999 2079.919678 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.900000 2080.171387 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.000000 2080.003662 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.100000 2080.087402 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.200001 2080.171387 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.299999 2080.003662 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.400000 2080.255371 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.500000 2080.507080 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.600000 2080.674805 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.700001 2080.758789 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.799999 2080.591064 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.900000 2080.758789 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.000000 2080.926514 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.100000 2080.926514 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.200001 2081.094238 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.299999 2081.262207 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.400000 2081.346191 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.500000 2081.429932 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.600000 2081.262207 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.700001 2081.597900 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.799999 2081.681641 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.900000 2081.513916 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.000000 2081.681641 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.100000 2082.101318 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.200001 2082.017334 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.299999 2081.849609 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.400000 2082.017334 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.500000 2082.101318 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.600000 2082.352783 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.700001 2082.269043 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.799999 2082.604736 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
597
unilabos/devices/donghua_ec/exports/3号机().txt
Normal file
597
unilabos/devices/donghua_ec/exports/3号机().txt
Normal file
@@ -0,0 +1,597 @@
|
||||
Time(s) E(mV) I(mA) Q(mC) Capacity(mAh) Energy(Wh) P(W)
|
||||
0.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
Time(s) E(mV) I(mA) Q(mC) Capacity(mAh) Energy(Wh) P(W)
|
||||
0.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
Time(s) E(mV) I(mA) Q(mC) Capacity(mAh) Energy(Wh) P(W)
|
||||
0.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
101
unilabos/devices/donghua_ec/exports/开路电位/0号机(开路电位).txt
Normal file
101
unilabos/devices/donghua_ec/exports/开路电位/0号机(开路电位).txt
Normal file
@@ -0,0 +1,101 @@
|
||||
Time(s) E(mV) I(mA) Q(mC) Capacity(mAh) Energy(Wh) P(W)
|
||||
0.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
981
unilabos/devices/donghua_ec/exports/循环伏安/2号机(循环伏安(多循环)).txt
Normal file
981
unilabos/devices/donghua_ec/exports/循环伏安/2号机(循环伏安(多循环)).txt
Normal file
@@ -0,0 +1,981 @@
|
||||
Time(s) E(mV) I(mA)
|
||||
0.010000 -997.723450 -5.245421
|
||||
0.020000 -996.045349 -5.010738
|
||||
0.030000 -993.947693 -4.961776
|
||||
0.040000 -991.682251 -4.928009
|
||||
0.050000 -989.752441 -4.899306
|
||||
0.060000 -987.990417 -5.074349
|
||||
0.070000 -985.641052 -4.800336
|
||||
0.080000 -983.879028 -4.896254
|
||||
0.090000 -981.697510 -4.842570
|
||||
0.100000 -979.935486 -4.868352
|
||||
0.110000 -978.005676 -4.946120
|
||||
0.120000 -975.656372 -4.801778
|
||||
0.130000 -973.978271 -4.739869
|
||||
0.140000 -971.880615 -4.706879
|
||||
0.150000 -969.866882 -4.946629
|
||||
0.160000 -967.769287 -4.942389
|
||||
0.170000 -966.007263 -4.839263
|
||||
0.180000 -963.657898 -4.812040
|
||||
0.190000 -961.811951 -4.663457
|
||||
0.200000 -959.798279 -4.709678
|
||||
0.210000 -957.784485 -4.658539
|
||||
0.220000 -955.938599 -4.624955
|
||||
0.230000 -953.757080 -4.610198
|
||||
0.240000 -951.995056 -4.587724
|
||||
0.250000 -949.897400 -4.594509
|
||||
0.260000 -947.883728 -4.307436
|
||||
0.270000 -946.037842 -4.310320
|
||||
0.280000 -943.940186 -4.356200
|
||||
0.290000 -941.926453 -4.364511
|
||||
0.300000 -939.661011 -4.595781
|
||||
0.310000 -937.731201 -4.543455
|
||||
0.320000 -935.717407 -4.559314
|
||||
0.330000 -933.871521 -4.562452
|
||||
0.340000 -931.606079 -4.427014
|
||||
0.350000 -929.844116 -4.489263
|
||||
0.360000 -927.662537 -4.452202
|
||||
0.370000 -925.732788 -4.399452
|
||||
0.380000 -923.886780 -4.405304
|
||||
0.390000 -921.873108 -4.408611
|
||||
0.400000 -919.691589 -4.333557
|
||||
0.410000 -917.593933 -4.334066
|
||||
0.420000 -915.831909 -4.386562
|
||||
0.430000 -913.482544 -4.543200
|
||||
0.440000 -913.146973 -4.722992
|
||||
0.450000 -909.622925 -4.428881
|
||||
0.460000 -907.777039 -4.410053
|
||||
0.470000 -905.679382 -4.363664
|
||||
0.480000 -903.581787 -4.374688
|
||||
0.490000 -901.735840 -4.188112
|
||||
0.500000 -899.973816 -3.465724
|
||||
0.510000 -898.127930 -3.826409
|
||||
0.520000 -895.694702 -3.733291
|
||||
0.530000 -893.848755 -3.535859
|
||||
0.540000 -891.751160 -3.213846
|
||||
0.550000 -889.821289 -3.182298
|
||||
0.560000 -888.059265 -2.966124
|
||||
0.570000 -885.961731 -2.916851
|
||||
0.580000 -883.360596 -3.444437
|
||||
0.590000 -881.682495 -3.675622
|
||||
0.600000 -880.004395 -3.014973
|
||||
0.610000 -877.738953 -2.964597
|
||||
0.620000 -875.893066 -2.955438
|
||||
0.630000 -873.543701 -3.789857
|
||||
0.640000 -871.781738 -2.904723
|
||||
0.650000 -869.767944 -3.019892
|
||||
0.660000 -867.838196 -2.876482
|
||||
0.670000 -865.572754 -2.871055
|
||||
0.680000 -863.894653 -2.873005
|
||||
0.690000 -861.796997 -2.856468
|
||||
0.700000 -859.699341 -2.857570
|
||||
0.710000 -857.685669 -3.277282
|
||||
0.720000 -855.755798 -3.240815
|
||||
0.730000 -853.909912 -2.812538
|
||||
0.740000 -851.812256 -2.800071
|
||||
0.750000 -849.714661 -2.830602
|
||||
0.760000 -847.617004 -2.861980
|
||||
0.770000 -845.687134 -2.828651
|
||||
0.780000 -843.673462 -2.989106
|
||||
0.790000 -841.575806 -3.356577
|
||||
0.800000 -839.981567 -3.497866
|
||||
0.810000 -837.212769 -3.823356
|
||||
0.820000 -835.534668 -3.269310
|
||||
0.830000 -833.437012 -3.710393
|
||||
0.840000 -831.842834 -2.778360
|
||||
0.850000 -829.829041 -2.802276
|
||||
0.860000 -827.815369 -2.759279
|
||||
0.870000 -825.717712 -2.751392
|
||||
0.880000 -823.787903 -2.733328
|
||||
0.890000 -821.942017 -2.720352
|
||||
0.900000 -819.844360 -2.750289
|
||||
0.910000 -817.830627 -2.919310
|
||||
0.920000 -815.733032 -2.894377
|
||||
0.930000 -813.803162 -2.898023
|
||||
0.940000 -811.789490 -2.866390
|
||||
0.950000 -809.859619 -2.864864
|
||||
0.960000 -807.510315 -3.323078
|
||||
0.970000 -805.916077 -2.828821
|
||||
0.980000 -803.566772 -2.821018
|
||||
0.990000 -801.888611 -2.812368
|
||||
1.000000 -799.874939 -2.779124
|
||||
1.010000 -797.777283 -2.807873
|
||||
1.020000 -795.847534 -2.797611
|
||||
1.030000 -793.498108 -2.787180
|
||||
1.040000 -791.903992 -2.775138
|
||||
1.050000 -789.974121 -2.762417
|
||||
1.060000 -787.792542 -2.723151
|
||||
1.070000 -785.694946 -2.718826
|
||||
1.080000 -784.016846 -2.685411
|
||||
1.090000 -782.003113 -2.675913
|
||||
1.100000 -779.653748 -2.651997
|
||||
1.110000 -777.807861 -2.652761
|
||||
1.120000 -775.877991 -2.638598
|
||||
1.130000 -773.864319 -2.628506
|
||||
1.140000 -771.682800 -2.617651
|
||||
1.150000 -769.501221 -2.629269
|
||||
1.160000 -767.907043 -2.609424
|
||||
1.170000 -765.809387 -2.601622
|
||||
1.180000 -763.879578 -2.593480
|
||||
1.190000 -761.698059 -2.583049
|
||||
1.200000 -759.600464 -2.560490
|
||||
1.210000 -757.586731 -2.555995
|
||||
1.220000 -755.740784 -2.563713
|
||||
1.230000 -753.894836 -2.534794
|
||||
1.240000 -752.048950 -2.521224
|
||||
1.250000 -749.699585 -2.530044
|
||||
1.260000 -747.685913 -2.603827
|
||||
1.270000 -745.672180 -2.536320
|
||||
1.280000 -743.658386 -2.561847
|
||||
1.290000 -741.728638 -2.517239
|
||||
1.300000 -739.798828 -2.482468
|
||||
1.310000 -737.868958 -2.469068
|
||||
1.320000 -736.023071 -2.464319
|
||||
1.330000 -733.925415 -2.457025
|
||||
1.340000 -731.659973 -2.449054
|
||||
1.350000 -729.981873 -2.444898
|
||||
1.360000 -727.800293 -2.441421
|
||||
1.370000 -725.618835 -2.437435
|
||||
1.380000 -723.688965 -2.433025
|
||||
1.390000 -721.759216 -2.430905
|
||||
1.400000 -719.661560 -2.424205
|
||||
1.410000 -717.899536 -2.414367
|
||||
1.420000 -715.718018 -2.416233
|
||||
1.430000 -713.788208 -2.403003
|
||||
1.440000 -711.522766 -2.391639
|
||||
1.450000 -709.760742 -2.383328
|
||||
1.460000 -707.998657 -2.383158
|
||||
1.470000 -705.984985 -2.372218
|
||||
1.480000 -703.635681 -2.377137
|
||||
1.490000 -701.705811 -2.377391
|
||||
1.500000 -699.776001 -2.370183
|
||||
1.510000 -697.846252 -2.365434
|
||||
1.520000 -695.748596 -2.726034
|
||||
1.530000 -693.734802 -2.722727
|
||||
1.540000 -691.888916 -2.729681
|
||||
1.550000 -689.707397 -2.716281
|
||||
1.560000 -687.777588 -2.697030
|
||||
1.570000 -685.763794 -2.677355
|
||||
1.580000 -684.001831 -2.668111
|
||||
1.590000 -681.652466 -2.662683
|
||||
1.600000 -679.890503 -2.646909
|
||||
1.610000 -677.625061 -2.631474
|
||||
1.620000 -675.611267 -2.621382
|
||||
1.630000 -673.849304 -3.134382
|
||||
1.640000 -671.919495 -2.953148
|
||||
1.650000 -669.905762 -2.917614
|
||||
1.660000 -667.975952 -2.634273
|
||||
1.670000 -665.710510 -2.670146
|
||||
1.680000 -663.864624 -2.665312
|
||||
1.690000 -661.431335 -2.793032
|
||||
1.700000 -659.753174 -2.684648
|
||||
1.710000 -657.655640 -2.447781
|
||||
1.720000 -655.809631 -2.459400
|
||||
1.730000 -653.795959 -2.214307
|
||||
1.740000 -651.782227 -2.323878
|
||||
1.750000 -649.852417 -2.461605
|
||||
1.760000 -648.174316 -2.134334
|
||||
1.770000 -645.741089 -2.281559
|
||||
1.780000 -643.727356 -2.102955
|
||||
1.790000 -641.881409 -2.146970
|
||||
1.800000 -639.615967 -2.266039
|
||||
1.810000 -637.518311 -2.363737
|
||||
1.820000 -635.924133 -2.080481
|
||||
1.830000 -633.742554 -2.538271
|
||||
1.840000 -631.812805 -2.472121
|
||||
1.850000 -630.218567 -2.064707
|
||||
1.860000 -627.869263 -2.240512
|
||||
1.870000 -625.687683 -2.340161
|
||||
1.880000 -623.925720 -2.479245
|
||||
1.890000 -621.911987 -2.518935
|
||||
1.900000 -619.898254 -2.519359
|
||||
1.910000 -617.716675 -2.306577
|
||||
1.920000 -615.786865 -2.513168
|
||||
1.930000 -613.689270 -2.541833
|
||||
1.940000 -611.675537 -2.324556
|
||||
1.950000 -609.745728 -2.314549
|
||||
1.960000 -607.815857 -2.303355
|
||||
1.970000 -605.634399 -2.315228
|
||||
1.980000 -603.704529 -2.314719
|
||||
1.990000 -601.858643 -2.302506
|
||||
2.000000 -599.593201 -2.500277
|
||||
2.010000 -597.663391 -2.252131
|
||||
2.020000 -595.817444 -2.413774
|
||||
2.030000 -593.803711 -2.275283
|
||||
2.040000 -591.622192 -2.237290
|
||||
2.050000 -589.608459 -2.236442
|
||||
2.060000 -587.510864 -2.239834
|
||||
2.070000 -585.832764 -2.233049
|
||||
2.080000 -583.567322 -2.228130
|
||||
2.090000 -581.805298 -2.237968
|
||||
2.100000 -579.791565 -2.244583
|
||||
2.110000 -577.777832 -2.200483
|
||||
2.120000 -575.848022 -2.377306
|
||||
2.130000 -573.666504 -2.203367
|
||||
2.140000 -571.820557 -2.181995
|
||||
2.150000 -569.639038 -2.179366
|
||||
2.160000 -567.960938 -2.190815
|
||||
2.170000 -565.779419 -2.180808
|
||||
2.180000 -563.513977 -2.163507
|
||||
2.190000 -561.835815 -2.375780
|
||||
2.200000 -559.654297 -2.135267
|
||||
2.210000 -557.808411 -2.128567
|
||||
2.220000 -555.542969 -2.308697
|
||||
2.230000 -553.613159 -2.307595
|
||||
2.240000 -551.767212 -2.303100
|
||||
2.250000 -549.417847 -2.100495
|
||||
2.260000 -548.075439 -2.131026
|
||||
2.270000 -545.642151 -2.087011
|
||||
2.280000 -543.880127 -2.080057
|
||||
2.290000 -541.614685 -2.073527
|
||||
2.300000 -539.768738 -2.067590
|
||||
2.310000 -537.755066 -2.068438
|
||||
2.320000 -535.657410 -2.062162
|
||||
2.330000 -533.727600 -2.055802
|
||||
2.340000 -531.546082 -2.048509
|
||||
2.350000 -530.035767 -2.043674
|
||||
2.360000 -527.854248 -2.043081
|
||||
2.370000 -525.756592 -2.047661
|
||||
2.380000 -523.658936 -2.039265
|
||||
2.390000 -521.813049 -2.032904
|
||||
2.400000 -519.715393 -2.150956
|
||||
2.410000 -517.953430 -1.847176
|
||||
2.420000 -515.771851 -1.889410
|
||||
2.430000 -513.842102 -1.841409
|
||||
2.440000 -511.492706 -1.839289
|
||||
2.450000 -509.730713 -1.833098
|
||||
2.460000 -507.633087 -1.832844
|
||||
2.470000 -505.787170 -1.824787
|
||||
2.480000 -503.689545 -1.819614
|
||||
2.490000 -501.843597 -1.815373
|
||||
2.500000 -499.829895 -1.807317
|
||||
2.510000 -497.816162 -1.817493
|
||||
2.520000 -495.802460 -1.808674
|
||||
2.530000 -493.872620 -1.803670
|
||||
2.540000 -491.691071 -1.797394
|
||||
2.550000 -489.845184 -1.784334
|
||||
2.560000 -487.915344 -1.786624
|
||||
2.570000 -485.733795 -1.790440
|
||||
2.580000 -483.804016 -1.777719
|
||||
2.590000 -481.706360 -1.768560
|
||||
2.600000 -479.524841 -2.182335
|
||||
2.610000 -477.595032 -2.161981
|
||||
2.620000 -475.749084 -2.157316
|
||||
2.630000 -473.819275 -2.149853
|
||||
2.640000 -471.721649 -2.136030
|
||||
2.650000 -469.875763 -2.134334
|
||||
2.660000 -467.862000 -1.757959
|
||||
2.670000 -465.512665 -1.756602
|
||||
2.680000 -463.582825 -1.761521
|
||||
2.690000 -461.736938 -1.756008
|
||||
2.700000 -459.723206 -1.751344
|
||||
2.710000 -457.793396 -1.748715
|
||||
2.720000 -455.611847 -1.743117
|
||||
2.730000 -453.849854 -1.735146
|
||||
2.740000 -451.668304 -1.734976
|
||||
2.750000 -449.654572 -1.741337
|
||||
2.760000 -447.808685 -1.721067
|
||||
2.770000 -445.627136 -1.710212
|
||||
2.780000 -443.697296 -1.699527
|
||||
2.790000 -442.019196 -1.702495
|
||||
2.800000 -439.585968 -1.699611
|
||||
2.810000 -437.823944 -1.703173
|
||||
2.820000 -435.726318 -1.711060
|
||||
2.830000 -433.544769 -1.708092
|
||||
2.840000 -431.866669 -1.703852
|
||||
2.850000 -429.852936 -1.702749
|
||||
2.860000 -427.671417 -1.695032
|
||||
2.870000 -425.825500 -1.695286
|
||||
2.880000 -423.560059 -2.050544
|
||||
2.890000 -421.546326 -1.687653
|
||||
2.900000 -419.868225 -1.682735
|
||||
2.910000 -417.518890 -2.099732
|
||||
2.920000 -415.672974 -1.665688
|
||||
2.930000 -413.659241 -1.658904
|
||||
2.940000 -411.813354 -1.656784
|
||||
2.950000 -409.463989 -1.658225
|
||||
2.960000 -407.618073 -1.653815
|
||||
2.970000 -405.856079 -1.647285
|
||||
2.980000 -403.758423 -1.653815
|
||||
2.990000 -401.576874 -1.646013
|
||||
3.000000 -399.814880 -1.641349
|
||||
3.010000 -397.801147 -1.643723
|
||||
3.020000 -395.619598 -1.639144
|
||||
3.030000 -393.857605 -1.638720
|
||||
3.040000 -392.011719 -1.634055
|
||||
3.050000 -389.578461 -1.647116
|
||||
3.060000 -387.732544 -1.625490
|
||||
3.070000 -385.634888 -1.627779
|
||||
3.080000 -383.705078 -1.600980
|
||||
3.090000 -381.859161 -1.595637
|
||||
3.100000 -379.845459 -1.591312
|
||||
3.110000 -377.747803 -1.587496
|
||||
3.120000 -375.650177 -1.584782
|
||||
3.130000 -373.804260 -1.583595
|
||||
3.140000 -371.538818 -1.574605
|
||||
3.150000 -369.525085 -1.561799
|
||||
3.160000 -367.846985 -1.559086
|
||||
3.170000 -365.917206 -1.556626
|
||||
3.180000 -363.735657 -1.562902
|
||||
3.190000 -361.889709 -1.554676
|
||||
3.200000 -359.792114 -1.550605
|
||||
3.210000 -357.694489 -1.556541
|
||||
3.220000 -355.596832 -1.542887
|
||||
3.230000 -353.834839 -1.540089
|
||||
3.240000 -351.569397 -1.535000
|
||||
3.250000 -349.807404 -1.529233
|
||||
3.260000 -347.625854 -1.527198
|
||||
3.270000 -345.528229 -1.526265
|
||||
3.280000 -343.682281 -1.525671
|
||||
3.290000 -341.416870 -1.520837
|
||||
3.300000 -339.570953 -1.515410
|
||||
3.310000 -337.389404 -1.511678
|
||||
3.320000 -335.963013 -1.508201
|
||||
3.330000 -333.613678 -1.503197
|
||||
3.340000 -331.599945 -1.501247
|
||||
3.350000 -329.754028 -1.499296
|
||||
3.360000 -327.656433 -1.494971
|
||||
3.370000 -325.558777 -1.491240
|
||||
3.380000 -323.712860 -1.487339
|
||||
3.390000 -321.783051 -1.486660
|
||||
3.400000 -320.021057 -1.483437
|
||||
3.410000 -317.671661 -1.479536
|
||||
3.420000 -315.741852 -1.478603
|
||||
3.430000 -313.812042 -1.473854
|
||||
3.440000 -311.462708 -1.471734
|
||||
3.450000 -309.868500 -1.466306
|
||||
3.460000 -307.770874 -1.463169
|
||||
3.470000 -305.673248 -1.458334
|
||||
3.480000 -303.491699 -1.455366
|
||||
3.490000 -301.729706 -1.451465
|
||||
3.500000 -299.799866 -1.449260
|
||||
3.510000 -297.702240 -1.449175
|
||||
3.520000 -295.604614 -1.445444
|
||||
3.530000 -293.674805 -1.441712
|
||||
3.540000 -291.744995 -1.438829
|
||||
3.550000 -289.563446 -1.436030
|
||||
3.560000 -287.633636 -1.432723
|
||||
3.570000 -285.619873 -1.430602
|
||||
3.580000 -283.690094 -1.427719
|
||||
3.590000 -281.844177 -1.422800
|
||||
3.600000 -279.662628 -1.423055
|
||||
3.610000 -277.816711 -1.418051
|
||||
3.620000 -275.802979 -1.414659
|
||||
3.630000 -273.705353 -1.413387
|
||||
3.640000 -271.523834 -1.409825
|
||||
3.650000 -269.761810 -1.407111
|
||||
3.660000 -267.915894 -1.406348
|
||||
3.670000 -265.650452 -1.401344
|
||||
3.680000 -263.636719 -1.399054
|
||||
3.690000 -261.790802 -1.394899
|
||||
3.700000 -259.273651 -1.391591
|
||||
3.710000 -257.595551 -1.389556
|
||||
3.720000 -255.665741 -1.385231
|
||||
3.730000 -253.652008 -1.383195
|
||||
3.740000 -251.722183 -1.377852
|
||||
3.750000 -249.540665 -1.376156
|
||||
3.760000 -247.778641 -1.371661
|
||||
3.770000 -245.681000 -1.368608
|
||||
3.780000 -243.415573 -1.366064
|
||||
3.790000 -241.821365 -1.364877
|
||||
3.800000 -239.807648 -1.362248
|
||||
3.810000 -237.626129 -1.359449
|
||||
3.820000 -235.612381 -1.355378
|
||||
3.830000 -233.514771 -1.354361
|
||||
3.840000 -231.668854 -1.349357
|
||||
3.850000 -229.655121 -1.345541
|
||||
3.860000 -227.557480 -1.344269
|
||||
3.870000 -225.543762 -1.340961
|
||||
3.880000 -223.613953 -1.337993
|
||||
3.890000 -221.600204 -1.335449
|
||||
3.900000 -219.586487 -1.332904
|
||||
3.910000 -217.572769 -1.329258
|
||||
3.920000 -215.726852 -1.328325
|
||||
3.930000 -213.713135 -1.323576
|
||||
3.940000 -211.867218 -1.321201
|
||||
3.950000 -209.769592 -1.318318
|
||||
3.960000 -207.588058 -1.316876
|
||||
3.970000 -205.658234 -1.314586
|
||||
3.980000 -203.644516 -1.310091
|
||||
3.990000 -201.714691 -1.307717
|
||||
4.000000 -199.700958 -1.304155
|
||||
4.010000 -197.603333 -1.301695
|
||||
4.020000 -195.757416 -1.298473
|
||||
4.030000 -193.491974 -1.295504
|
||||
4.040000 -191.729965 -1.293130
|
||||
4.050000 -189.716232 -1.291518
|
||||
4.060000 -187.702515 -1.289568
|
||||
4.070000 -185.688797 -1.284649
|
||||
4.080000 -183.675064 -1.283801
|
||||
4.090000 -181.745239 -1.278967
|
||||
4.100000 -179.647614 -1.278458
|
||||
4.110000 -177.633896 -1.277356
|
||||
4.120000 -175.620163 -1.272691
|
||||
4.130000 -173.690338 -1.268451
|
||||
4.140000 -171.676620 -1.267094
|
||||
4.150000 -169.662903 -1.263702
|
||||
4.160000 -167.900879 -1.262005
|
||||
4.170000 -165.719345 -1.259461
|
||||
4.180000 -163.537827 -1.256154
|
||||
4.190000 -161.608002 -1.254373
|
||||
4.200000 -159.845993 -1.249115
|
||||
4.210000 -157.748367 -1.248606
|
||||
4.220000 -155.482910 -1.245129
|
||||
4.230000 -153.888718 -1.243348
|
||||
4.240000 -151.707184 -1.239871
|
||||
4.250000 -149.693451 -1.238090
|
||||
4.260000 -147.847549 -1.234274
|
||||
4.270000 -145.749908 -1.232069
|
||||
4.280000 -143.736191 -1.229609
|
||||
4.290000 -141.554657 -1.227998
|
||||
4.300000 -139.708740 -1.224521
|
||||
4.310000 -137.946732 -1.222740
|
||||
4.320000 -135.597382 -1.219178
|
||||
4.330000 -133.919281 -1.217058
|
||||
4.340000 -131.737732 -1.214344
|
||||
4.350000 -129.640106 -1.212648
|
||||
4.360000 -127.458580 -1.209255
|
||||
4.370000 -125.612663 -1.205693
|
||||
4.380000 -123.766747 -1.202810
|
||||
4.390000 -121.669113 -1.200351
|
||||
4.400000 -119.823204 -1.198485
|
||||
4.410000 -117.725563 -1.195516
|
||||
4.420000 -115.711845 -1.192209
|
||||
4.430000 -113.698128 -1.190428
|
||||
4.440000 -111.600487 -1.187460
|
||||
4.450000 -109.586761 -1.185764
|
||||
4.460000 -107.740845 -1.184068
|
||||
4.470000 -105.643219 -1.180082
|
||||
4.480000 -103.629494 -1.178385
|
||||
4.490000 -101.615768 -1.174739
|
||||
4.500000 -99.602043 -1.172534
|
||||
4.510000 -97.504417 -1.170753
|
||||
4.520000 -95.574600 -1.167445
|
||||
4.530000 -93.644768 -1.163459
|
||||
4.540000 -91.966675 -1.161848
|
||||
4.550000 -89.701233 -1.162272
|
||||
4.560000 -87.603600 -1.159473
|
||||
4.570000 -85.925491 -1.154724
|
||||
4.580000 -83.660049 -1.153876
|
||||
4.590000 -81.730232 -1.149551
|
||||
4.600000 -79.716515 -1.149890
|
||||
4.610000 -77.534973 -1.146074
|
||||
4.620000 -75.605156 -1.142003
|
||||
4.630000 -73.591431 -1.140985
|
||||
4.640000 -71.661606 -1.138865
|
||||
4.650000 -69.731789 -1.136999
|
||||
4.660000 -67.718063 -1.134540
|
||||
4.670000 -65.452621 -1.131572
|
||||
4.680000 -63.690613 -1.129621
|
||||
4.690000 -61.676888 -1.126144
|
||||
4.700000 -59.579258 -1.123430
|
||||
4.710000 -57.649437 -1.121225
|
||||
4.720000 -55.635715 -1.119614
|
||||
4.730000 -53.538082 -1.116985
|
||||
4.740000 -51.776077 -1.115543
|
||||
4.750000 -49.594540 -1.115798
|
||||
4.760000 -47.664719 -1.147431
|
||||
4.770000 -45.567089 -1.146667
|
||||
4.780000 -43.469460 -1.141240
|
||||
4.790000 -41.539642 -1.135727
|
||||
4.800000 -39.525917 -1.134540
|
||||
4.810000 -37.680000 -1.132081
|
||||
4.820000 -35.834084 -1.127671
|
||||
4.830000 -33.904266 -1.125635
|
||||
4.840000 -31.806633 -1.123345
|
||||
4.850000 -29.709005 -1.122582
|
||||
4.860000 -27.611376 -1.120547
|
||||
4.870000 -25.513744 -1.115628
|
||||
4.880000 -23.583923 -1.113678
|
||||
4.890000 -21.486296 -1.112914
|
||||
4.900000 -19.808189 -1.109352
|
||||
4.910000 -17.458843 -1.107487
|
||||
4.920000 -15.529024 -1.105536
|
||||
4.930000 -13.431395 -1.103077
|
||||
4.940000 -11.501575 -1.099091
|
||||
4.950000 -9.403944 -1.095020
|
||||
4.960000 -7.474124 -1.092645
|
||||
4.970000 -5.879925 -1.090016
|
||||
4.980000 -3.698390 -1.088914
|
||||
4.990000 -1.684664 -1.086709
|
||||
5.000000 0.412966 -1.084080
|
||||
5.010000 2.510596 -1.081620
|
||||
5.020000 4.524321 -1.077804
|
||||
5.030000 6.538046 -1.077550
|
||||
5.040000 8.635676 -1.074581
|
||||
5.050000 10.145969 -1.070765
|
||||
5.060000 12.327506 -1.068984
|
||||
5.070000 14.341230 -1.064404
|
||||
5.080000 16.438860 -1.065168
|
||||
5.090000 18.536491 -1.062454
|
||||
5.100000 20.466312 -1.057959
|
||||
5.110000 22.563942 -1.056857
|
||||
5.120000 24.493761 -1.054228
|
||||
5.130000 26.423580 -1.052871
|
||||
5.140000 28.353401 -1.052023
|
||||
5.150000 30.451031 -1.048376
|
||||
5.160000 32.464756 -1.046510
|
||||
5.170000 34.562386 -1.044644
|
||||
5.180000 36.408302 -1.042185
|
||||
5.190000 38.422028 -1.042354
|
||||
5.200000 40.435749 -1.040658
|
||||
5.210000 42.197762 -1.037181
|
||||
5.220000 44.295391 -1.035061
|
||||
5.230000 46.393021 -1.032178
|
||||
5.240000 48.155029 -1.032008
|
||||
5.250000 50.420471 -1.028361
|
||||
5.260000 52.685913 -1.025139
|
||||
5.270000 54.447922 -1.024291
|
||||
5.280000 56.293835 -1.022425
|
||||
5.290000 58.307560 -1.022001
|
||||
5.300000 60.321289 -1.017082
|
||||
5.310000 62.502819 -1.015640
|
||||
5.320000 64.516548 -1.013944
|
||||
5.330000 66.698082 -1.009873
|
||||
5.340000 68.376183 -1.006651
|
||||
5.350000 70.389908 -1.005463
|
||||
5.360000 72.571442 -1.003174
|
||||
5.370000 74.669075 -1.001562
|
||||
5.380000 76.514992 -0.999696
|
||||
5.390000 78.528709 -0.997831
|
||||
5.400000 80.626350 -0.995287
|
||||
5.410000 82.640068 -0.993675
|
||||
5.420000 84.485985 -0.992149
|
||||
5.430000 86.499710 -0.990028
|
||||
5.440000 88.513435 -0.987060
|
||||
5.450000 90.443260 -0.984686
|
||||
5.460000 92.205261 -0.982141
|
||||
5.470000 94.302895 -0.980869
|
||||
5.480000 96.316620 -0.977731
|
||||
5.490000 98.330345 -0.975102
|
||||
5.500000 100.427979 -0.975017
|
||||
5.510000 102.525604 -0.972558
|
||||
5.520000 104.455421 -0.970692
|
||||
5.530000 106.636963 -0.967300
|
||||
5.540000 108.231163 -0.966961
|
||||
5.550000 110.412689 -0.965180
|
||||
5.560000 112.510323 -0.963484
|
||||
5.570000 114.524055 -0.961024
|
||||
5.580000 116.705582 -0.958226
|
||||
5.590000 118.383690 -0.956869
|
||||
5.600000 120.397415 -0.954070
|
||||
5.610000 122.327240 -0.952374
|
||||
5.620000 124.508766 -0.950169
|
||||
5.630000 126.354691 -0.946861
|
||||
5.640000 128.620132 -0.946268
|
||||
5.650000 130.466049 -0.942706
|
||||
5.660000 132.395859 -0.941858
|
||||
5.670000 134.241776 -0.939568
|
||||
5.680000 136.507217 -0.937363
|
||||
5.690000 138.520935 -0.936006
|
||||
5.700000 140.534668 -0.932953
|
||||
5.710000 142.464493 -0.931172
|
||||
5.720000 144.646027 -0.930409
|
||||
5.730000 145.988510 -0.926932
|
||||
5.740000 148.421753 -0.926847
|
||||
5.750000 150.603287 -0.923200
|
||||
5.760000 152.617020 -0.922098
|
||||
5.770000 154.546844 -0.919469
|
||||
5.780000 156.392761 -0.917857
|
||||
5.790000 158.406479 -0.914974
|
||||
5.800000 160.336304 -0.912345
|
||||
5.810000 162.517838 -0.911412
|
||||
5.820000 164.363739 -0.910055
|
||||
5.830000 166.461380 -0.906663
|
||||
5.840000 168.475098 -0.903949
|
||||
5.850000 170.404922 -0.901235
|
||||
5.860000 172.502563 -0.899624
|
||||
5.870000 174.516281 -0.899115
|
||||
5.880000 176.613922 -0.899285
|
||||
5.890000 178.459824 -0.894705
|
||||
5.900000 180.725266 -0.892585
|
||||
5.910000 182.235550 -0.890889
|
||||
5.920000 184.500992 -0.888684
|
||||
5.930000 186.598633 -0.886055
|
||||
5.940000 188.612350 -0.885461
|
||||
5.950000 190.542175 -0.883426
|
||||
5.960000 192.304169 -0.880118
|
||||
5.970000 194.317917 -0.879694
|
||||
5.980000 196.499435 -0.876641
|
||||
5.990000 198.429276 -0.874182
|
||||
6.000000 200.442993 -0.874351
|
||||
6.010000 202.624512 -0.869517
|
||||
6.020000 204.470428 -0.869432
|
||||
6.030000 206.484146 -0.866803
|
||||
6.040000 208.413986 -0.864938
|
||||
6.050000 210.679428 -0.864005
|
||||
6.060000 212.525330 -0.863326
|
||||
6.070000 214.371246 -0.860188
|
||||
6.080000 216.384964 -0.860188
|
||||
6.090000 218.398712 -0.856796
|
||||
6.100000 220.412430 -0.854676
|
||||
6.110000 222.426147 -0.852217
|
||||
6.120000 224.355972 -0.850520
|
||||
6.130000 226.537506 -0.849672
|
||||
6.140000 228.299515 -0.848061
|
||||
6.150000 230.397141 -0.845347
|
||||
6.160000 232.578690 -0.843397
|
||||
6.170000 234.676300 -0.841785
|
||||
6.180000 236.270523 -0.840768
|
||||
6.190000 238.368134 -0.839750
|
||||
6.200000 240.381866 -0.836866
|
||||
6.210000 242.311676 -0.835764
|
||||
6.220000 244.577118 -0.833389
|
||||
6.230000 246.506943 -0.831693
|
||||
6.240000 248.520676 -0.830167
|
||||
6.250000 250.198776 -0.827538
|
||||
6.260000 252.380310 -0.825587
|
||||
6.270000 254.561844 -0.823212
|
||||
6.280000 256.407776 -0.821347
|
||||
6.290000 258.673218 -0.819905
|
||||
6.300000 260.351318 -0.816173
|
||||
6.310000 262.365021 -0.814986
|
||||
6.320000 264.462677 -0.814986
|
||||
6.330000 266.224670 -0.813205
|
||||
6.340000 268.574036 -0.810661
|
||||
6.350000 270.503845 -0.807269
|
||||
6.360000 272.433655 -0.805827
|
||||
6.370000 274.615173 -0.804809
|
||||
6.380000 276.712830 -0.804724
|
||||
6.390000 278.558746 -0.801841
|
||||
6.400000 280.488556 -0.799806
|
||||
6.410000 282.334473 -0.798364
|
||||
6.420000 284.683807 -0.796837
|
||||
6.430000 286.529724 -0.794802
|
||||
6.440000 288.543457 -0.792173
|
||||
6.450000 290.305450 -0.791749
|
||||
6.460000 292.487000 -0.787848
|
||||
6.470000 294.416809 -0.786321
|
||||
6.480000 296.430542 -0.784795
|
||||
6.490000 298.695984 -0.783183
|
||||
6.500000 300.290192 -0.781233
|
||||
6.510000 302.387817 -0.779452
|
||||
6.520000 304.401550 -0.778858
|
||||
6.530000 306.499176 -0.775975
|
||||
6.540000 308.680725 -0.774618
|
||||
6.550000 310.526611 -0.773346
|
||||
6.560000 312.288635 -0.771056
|
||||
6.570000 314.470184 -0.769275
|
||||
6.580000 316.735626 -0.767409
|
||||
6.590000 318.413727 -0.766561
|
||||
6.600000 320.343536 -0.765119
|
||||
6.610000 322.441162 -0.764695
|
||||
6.620000 324.538788 -0.762575
|
||||
6.630000 326.384705 -0.760540
|
||||
6.640000 328.482330 -0.759946
|
||||
6.650000 330.663879 -0.759353
|
||||
6.660000 332.425873 -0.756215
|
||||
6.670000 334.439606 -0.753586
|
||||
6.680000 336.621155 -0.752398
|
||||
6.690000 338.550964 -0.750448
|
||||
6.700000 340.564697 -0.748667
|
||||
6.710000 342.662292 -0.746886
|
||||
6.720000 344.424347 -0.744681
|
||||
6.730000 346.438049 -0.742900
|
||||
6.740000 348.535675 -0.740356
|
||||
6.750000 350.549408 -0.739084
|
||||
6.760000 352.647064 -0.739084
|
||||
6.770000 354.576843 -0.735691
|
||||
6.780000 356.674500 -0.734080
|
||||
6.790000 358.436493 -0.731451
|
||||
6.800000 360.534119 -0.729924
|
||||
6.810000 362.547882 -0.729331
|
||||
6.820000 364.477661 -0.727889
|
||||
6.830000 366.407471 -0.725854
|
||||
6.840000 368.589020 -0.724666
|
||||
6.850000 370.518860 -0.722292
|
||||
6.860000 372.532593 -0.734334
|
||||
6.870000 374.462402 -0.732045
|
||||
6.880000 376.560028 -0.729585
|
||||
6.890000 378.573730 -0.728737
|
||||
6.900000 380.419678 -0.728059
|
||||
6.910000 382.517303 -0.726447
|
||||
6.920000 384.531006 -0.724158
|
||||
6.930000 386.544739 -0.726278
|
||||
6.940000 388.558441 -0.723564
|
||||
6.950000 390.739990 -0.720596
|
||||
6.960000 392.585938 -0.717712
|
||||
6.970000 394.599640 -0.714659
|
||||
6.980000 396.361664 -0.715507
|
||||
6.990000 398.375366 -0.701344
|
||||
7.000000 400.305206 -0.699479
|
||||
7.010000 402.570648 -0.696680
|
||||
7.020000 404.668274 -0.698631
|
||||
7.030000 406.262482 -0.695832
|
||||
7.040000 408.444031 -0.695153
|
||||
7.050000 410.457733 -0.692864
|
||||
7.060000 412.555359 -0.691337
|
||||
7.070000 414.485168 -0.689471
|
||||
7.080000 416.582794 -0.688199
|
||||
7.090000 418.344788 -0.685231
|
||||
7.100000 420.442444 -0.685231
|
||||
7.110000 422.372284 -0.683365
|
||||
7.120000 424.637726 -0.681584
|
||||
7.130000 426.567505 -0.679464
|
||||
7.140000 428.581268 -0.678701
|
||||
7.150000 430.678894 -0.677259
|
||||
7.160000 432.608704 -0.674206
|
||||
7.170000 434.454590 -0.672764
|
||||
7.180000 436.720032 -0.671831
|
||||
7.190000 438.649872 -0.669796
|
||||
7.200000 440.579712 -0.670135
|
||||
7.210000 442.677307 -0.666913
|
||||
7.220000 444.607147 -0.665895
|
||||
7.230000 446.788696 -0.663775
|
||||
7.240000 448.550690 -0.662079
|
||||
7.250000 450.648315 -0.659534
|
||||
7.260000 452.745941 -0.658347
|
||||
7.270000 454.591858 -0.655972
|
||||
7.280000 456.437775 -0.653937
|
||||
7.290000 458.535400 -0.653598
|
||||
7.300000 460.465240 -0.651986
|
||||
7.310000 462.562866 -0.650460
|
||||
7.320000 464.492676 -0.648933
|
||||
7.330000 466.506378 -0.647237
|
||||
7.340000 468.687927 -0.644015
|
||||
7.350000 470.533875 -0.642234
|
||||
7.360000 472.379761 -0.642149
|
||||
7.370000 474.645203 -0.640113
|
||||
7.380000 476.407196 -0.637654
|
||||
7.390000 478.588745 -0.636297
|
||||
7.400000 480.518585 -0.634177
|
||||
7.410000 482.364471 -0.632226
|
||||
7.420000 484.462128 -0.630869
|
||||
7.430000 486.475861 -0.629173
|
||||
7.440000 488.573456 -0.627477
|
||||
7.450000 490.671112 -0.625272
|
||||
7.460000 492.768738 -0.625272
|
||||
7.470000 494.530731 -0.624509
|
||||
7.480000 496.628387 -0.622134
|
||||
7.490000 498.642120 -0.620523
|
||||
7.500000 500.571930 -0.618742
|
||||
7.510000 502.501709 -0.616113
|
||||
7.520000 504.767151 -0.614841
|
||||
7.530000 506.780914 -0.613654
|
||||
7.540000 508.626831 -0.612466
|
||||
7.550000 510.472748 -0.610940
|
||||
7.560000 512.318665 -0.609752
|
||||
7.570000 514.500183 -0.608226
|
||||
7.580000 516.346069 -0.607039
|
||||
7.590000 518.359802 -0.605427
|
||||
7.600000 520.373596 -0.602629
|
||||
7.610000 522.471191 -0.601441
|
||||
7.620000 524.568787 -0.600254
|
||||
7.630000 526.750366 -0.598812
|
||||
7.640000 528.764038 -0.597286
|
||||
7.650000 530.609985 -0.595844
|
||||
7.660000 532.623718 -0.593639
|
||||
7.670000 534.553528 -0.593215
|
||||
7.680000 536.567261 -0.591688
|
||||
7.690000 538.664856 -0.590332
|
||||
7.700000 540.510803 -0.588805
|
||||
7.710000 542.440613 -0.586685
|
||||
7.720000 544.538269 -0.585667
|
||||
7.730000 546.468079 -0.584904
|
||||
7.740000 548.565674 -0.583971
|
||||
7.750000 550.327698 -0.581427
|
||||
7.760000 552.677063 -0.581088
|
||||
7.770000 554.522949 -0.578374
|
||||
7.780000 556.620605 -0.577017
|
||||
7.790000 558.550415 -0.575066
|
||||
7.800000 560.648071 -0.574727
|
||||
7.810000 562.577881 -0.572776
|
||||
7.820000 564.507690 -0.570656
|
||||
7.830000 566.353638 -0.568790
|
||||
7.840000 568.283447 -0.569384
|
||||
7.850000 570.464966 -0.566077
|
||||
7.860000 572.478699 -0.564720
|
||||
7.870000 574.492432 -0.563108
|
||||
7.880000 576.673950 -0.562006
|
||||
7.890000 578.435974 -0.560055
|
||||
7.900000 580.617493 -0.558359
|
||||
7.910000 582.463440 -0.557172
|
||||
7.920000 584.728882 -0.555730
|
||||
7.930000 586.574768 -0.553186
|
||||
7.940000 588.672424 -0.552253
|
||||
7.950000 590.434387 -0.550811
|
||||
7.960000 592.364258 -0.548776
|
||||
7.970000 594.461853 -0.546401
|
||||
7.980000 596.727295 -0.546147
|
||||
7.990000 598.741028 -0.544536
|
||||
8.000000 600.502991 -0.542755
|
||||
8.010000 602.432861 -0.543263
|
||||
8.020000 604.446533 -0.540295
|
||||
8.030000 606.628113 -0.540465
|
||||
8.040000 608.641846 -0.538514
|
||||
8.050000 610.487732 -0.536818
|
||||
8.060000 612.585388 -0.535546
|
||||
8.070000 614.682983 -0.533171
|
||||
8.080000 616.361084 -0.531136
|
||||
8.090000 618.542664 -0.529864
|
||||
8.100000 620.388550 -0.529610
|
||||
8.110000 622.570068 -0.528592
|
||||
8.120000 624.583801 -0.526387
|
||||
8.130000 626.597595 -0.526013
|
||||
8.140000 628.527344 -0.524972
|
||||
8.150000 630.625000 -0.523219
|
||||
8.160000 632.722656 -0.521918
|
||||
8.170000 634.484680 -0.519963
|
||||
8.180000 636.414429 -0.519291
|
||||
8.190000 638.679871 -0.517773
|
||||
8.200000 640.525757 -0.516850
|
||||
8.210000 642.455627 -0.514760
|
||||
8.220000 644.553223 -0.513535
|
||||
8.230000 646.399170 -0.512100
|
||||
8.240000 648.580750 -0.511270
|
||||
8.250000 650.510559 -0.509365
|
||||
8.260000 652.356445 -0.508333
|
||||
8.270000 654.537964 -0.507527
|
||||
8.280000 656.803406 -0.505253
|
||||
8.290000 658.733215 -0.504330
|
||||
8.300000 660.579163 -0.502526
|
||||
8.310000 662.592834 -0.501083
|
||||
8.320000 664.606628 -0.499715
|
||||
8.330000 666.620300 -0.498524
|
||||
8.340000 668.466248 -0.497433
|
||||
8.350000 670.563843 -0.496761
|
||||
8.360000 672.409851 -0.494529
|
||||
8.370000 674.423523 -0.492885
|
||||
8.380000 676.521179 -0.492138
|
||||
8.390000 678.702698 -0.495855
|
||||
8.400000 680.548584 -0.494278
|
||||
8.410000 682.562378 -0.493279
|
||||
8.420000 684.995605 -0.492364
|
||||
8.430000 686.589783 -0.489847
|
||||
8.440000 688.435669 -0.489058
|
||||
8.450000 690.533325 -0.487422
|
||||
8.460000 692.463135 -0.485920
|
||||
8.470000 694.476868 -0.484552
|
||||
8.480000 696.490601 -0.483747
|
||||
8.490000 698.336487 -0.481993
|
||||
8.500000 700.518005 -0.480189
|
||||
8.510000 702.363892 -0.479224
|
||||
8.520000 704.629333 -0.477932
|
||||
8.530000 706.643127 -0.477109
|
||||
8.540000 708.740784 -0.475498
|
||||
8.550000 710.670532 -0.474760
|
||||
8.560000 712.768188 -0.473249
|
||||
8.570000 714.614136 -0.471731
|
||||
8.580000 716.627869 -0.469322
|
||||
8.590000 718.725464 -0.467460
|
||||
8.600000 720.823120 -0.466318
|
||||
8.610000 722.417297 -0.464917
|
||||
8.620000 724.682739 -0.463969
|
||||
8.630000 726.528625 -0.463516
|
||||
8.640000 728.542419 -0.461443
|
||||
8.650000 730.472168 -0.460713
|
||||
8.660000 732.569824 -0.458523
|
||||
8.670000 734.667419 -0.457449
|
||||
8.680000 736.597290 -0.455796
|
||||
8.690000 738.443176 -0.454688
|
||||
8.700000 740.624756 -0.453447
|
||||
8.710000 742.470642 -0.451945
|
||||
8.720000 744.568298 -0.451013
|
||||
8.730000 746.414185 -0.449645
|
||||
8.740000 748.595703 -0.448353
|
||||
8.750000 750.609497 -0.447036
|
||||
8.760000 752.707031 -0.446490
|
||||
8.770000 754.804688 -0.444829
|
||||
8.780000 756.734497 -0.443377
|
||||
8.790000 758.580444 -0.442093
|
||||
8.800000 760.510254 -0.440885
|
||||
8.810000 762.859619 -0.439828
|
||||
8.820000 764.621582 -0.438158
|
||||
8.830000 766.551453 -0.437436
|
||||
8.840000 768.481262 -0.435917
|
||||
8.850000 770.662781 -0.435145
|
||||
8.860000 772.676575 -0.433669
|
||||
8.870000 774.522461 -0.433845
|
||||
8.880000 776.703979 -0.432024
|
||||
8.890000 778.382080 -0.430631
|
||||
8.900000 780.731445 -0.428542
|
||||
8.910000 782.577332 -0.427124
|
||||
8.920000 784.423218 -0.425924
|
||||
8.930000 786.604797 -0.424363
|
||||
8.940000 788.534607 -0.423171
|
||||
8.950000 790.380554 -0.421418
|
||||
8.960000 792.645996 -0.420973
|
||||
8.970000 794.407959 -0.420042
|
||||
8.980000 796.505615 -0.418514
|
||||
8.990000 798.435425 -0.417541
|
||||
9.000000 800.616943 -0.416870
|
||||
9.010000 802.798523 -0.415947
|
||||
9.020000 804.644409 -0.416182
|
||||
9.030000 806.574219 -0.413958
|
||||
9.040000 808.587952 -0.415066
|
||||
9.050000 810.685608 -0.416735
|
||||
9.060000 812.699280 -0.413194
|
||||
9.070000 814.964722 -0.410753
|
||||
9.080000 816.642822 -0.411155
|
||||
9.090000 818.740479 -0.408739
|
||||
9.100000 820.670288 -0.408059
|
||||
9.110000 822.516174 -0.408218
|
||||
9.120000 824.446045 -0.407153
|
||||
9.130000 826.627563 -0.403654
|
||||
9.140000 828.557373 -0.404342
|
||||
9.150000 830.571106 -0.402345
|
||||
9.160000 832.584839 -0.401296
|
||||
9.170000 834.514709 -0.399777
|
||||
9.180000 836.528381 -0.398879
|
||||
9.190000 838.458252 -0.398309
|
||||
9.200000 840.639709 -0.398300
|
||||
9.210000 842.569580 -0.396538
|
||||
9.220000 844.583252 -0.397914
|
||||
9.230000 846.596985 -0.393895
|
||||
9.240000 848.526794 -0.392938
|
||||
9.250000 850.624451 -0.392552
|
||||
9.260000 852.638184 -0.424145
|
||||
9.270000 854.567993 -0.424052
|
||||
9.280000 856.581726 -0.420788
|
||||
9.290000 858.511536 -0.420042
|
||||
9.300000 860.860901 -0.421015
|
||||
9.310000 862.622864 -0.420092
|
||||
9.320000 864.636658 -0.418338
|
||||
9.330000 866.482544 -0.417247
|
||||
9.340000 868.412354 -0.416299
|
||||
9.350000 870.258301 -0.413538
|
||||
9.360000 872.439819 -0.413211
|
||||
9.370000 874.537415 -0.412473
|
||||
9.380000 876.299438 -0.410610
|
||||
9.390000 878.564880 -0.410249
|
||||
9.400000 880.578613 -0.407363
|
||||
9.410000 882.844055 -0.406339
|
||||
9.420000 884.438293 -0.405542
|
||||
9.430000 886.535828 -0.404040
|
||||
9.440000 888.633484 -0.402722
|
||||
9.450000 890.647278 -0.402638
|
||||
9.460000 892.744812 -0.401296
|
||||
9.470000 894.758606 -0.398409
|
||||
9.480000 896.604492 -0.397897
|
||||
9.490000 898.450378 -0.396622
|
||||
9.500000 900.715820 -0.396286
|
||||
9.510000 902.310059 -0.394558
|
||||
9.520000 904.659363 -0.392762
|
||||
9.530000 906.673157 -0.390757
|
||||
9.540000 908.519043 -0.390027
|
||||
9.550000 910.784485 -0.387694
|
||||
9.560000 912.630432 -0.387434
|
||||
9.570000 914.560242 -0.385839
|
||||
9.580000 916.406128 -0.384572
|
||||
9.590000 918.755493 -0.382970
|
||||
9.600000 920.937012 -0.382651
|
||||
9.610000 922.531250 -0.381283
|
||||
9.620000 924.544983 -0.381224
|
||||
9.630000 926.642578 -0.378833
|
||||
9.640000 928.656311 -0.376651
|
||||
9.650000 930.502197 -0.376148
|
||||
9.660000 932.767639 -0.374620
|
||||
9.670000 934.529663 -0.374931
|
||||
9.680000 936.375549 -0.373823
|
||||
9.690000 938.724976 -0.372539
|
||||
9.700000 940.654724 -0.370668
|
||||
9.710000 942.584534 -0.369754
|
||||
9.720000 944.598267 -0.367992
|
||||
9.730000 946.528076 -0.366305
|
||||
9.740000 948.877441 -0.364803
|
||||
9.750000 950.471619 -0.363611
|
||||
9.760000 952.485413 -0.362269
|
||||
9.770000 954.666931 -0.361220
|
||||
9.780000 956.764526 -0.359802
|
||||
9.790000 958.610474 -0.358510
|
||||
9.800000 960.624146 -0.356764
|
||||
1913
unilabos/devices/donghua_ec/exports/恒电流间歇滴定法/2号机(恒电流间歇滴定法(GITT)).txt
Normal file
1913
unilabos/devices/donghua_ec/exports/恒电流间歇滴定法/2号机(恒电流间歇滴定法(GITT)).txt
Normal file
File diff suppressed because it is too large
Load Diff
199
unilabos/devices/donghua_ec/exports/线性扫描/2号机(线性扫描(LSV)).txt
Normal file
199
unilabos/devices/donghua_ec/exports/线性扫描/2号机(线性扫描(LSV)).txt
Normal file
@@ -0,0 +1,199 @@
|
||||
Time(s) E(mV) I(mA)
|
||||
0.100000 1.763577 -0.029101
|
||||
0.200000 2.762724 -0.028610
|
||||
0.300000 3.770268 -0.028559
|
||||
0.400000 4.786208 -0.028826
|
||||
0.500000 5.768563 -0.028349
|
||||
0.600000 6.792899 -0.028234
|
||||
0.700000 7.775254 -0.028022
|
||||
0.800000 8.782798 -0.028118
|
||||
0.900000 9.765153 -0.028299
|
||||
1.000000 10.789489 -0.028453
|
||||
1.100000 11.780240 -0.028453
|
||||
1.200000 12.821369 -0.028657
|
||||
1.300000 13.770139 -0.028915
|
||||
1.400000 14.777682 -0.028546
|
||||
1.500000 15.776831 -0.029188
|
||||
1.600000 16.767582 -0.028683
|
||||
1.700000 17.783520 -0.028601
|
||||
1.800000 18.782669 -0.028970
|
||||
1.900000 19.807005 -0.028870
|
||||
2.000000 20.797756 -0.028791
|
||||
2.100000 21.805300 -0.028462
|
||||
2.200000 22.737278 -0.028364
|
||||
2.300000 23.778406 -0.028617
|
||||
2.400000 24.752365 -0.028738
|
||||
2.500000 25.793493 -0.029199
|
||||
2.600000 26.767452 -0.028708
|
||||
2.700000 27.783392 -0.028065
|
||||
2.800000 28.799332 -0.027991
|
||||
2.900000 29.806875 -0.028424
|
||||
3.000000 30.780836 -0.028417
|
||||
3.100000 31.796772 -0.028475
|
||||
3.200000 32.787525 -0.028331
|
||||
3.300000 33.778278 -0.028108
|
||||
3.400000 34.769028 -0.028230
|
||||
3.500000 35.784969 -0.027984
|
||||
3.600000 36.784115 -0.027942
|
||||
3.700000 37.766468 -0.028032
|
||||
3.800000 38.790806 -0.028286
|
||||
3.900000 39.789955 -0.028354
|
||||
4.000000 40.763912 -0.028207
|
||||
4.100000 41.754665 -0.028099
|
||||
4.200000 42.770603 -0.028197
|
||||
4.300000 43.786545 -0.028176
|
||||
4.400000 44.777294 -0.028192
|
||||
4.500000 45.784836 -0.028307
|
||||
4.600000 46.775589 -0.028396
|
||||
4.700000 47.774738 -0.028483
|
||||
4.800000 48.773884 -0.028376
|
||||
4.900000 49.781429 -0.028730
|
||||
5.000000 50.780575 -0.029071
|
||||
5.100000 51.788116 -0.029210
|
||||
5.200000 52.770473 -0.028525
|
||||
5.300000 53.752831 -0.028479
|
||||
5.400000 54.768772 -0.029232
|
||||
5.500000 55.793106 -0.029103
|
||||
5.600000 56.800652 -0.029043
|
||||
5.700000 57.766212 -0.029321
|
||||
5.800000 58.765362 -0.029204
|
||||
5.900000 59.789696 -0.029369
|
||||
6.000000 60.797237 -0.029143
|
||||
6.100000 61.787991 -0.028959
|
||||
6.200000 62.803932 -0.028818
|
||||
6.300000 63.769493 -0.029102
|
||||
6.400000 64.768639 -0.029005
|
||||
6.500000 65.759392 -0.028507
|
||||
6.600000 66.783730 -0.029151
|
||||
6.700000 67.774475 -0.028881
|
||||
6.800000 68.790421 -0.028720
|
||||
6.900000 69.789566 -0.028786
|
||||
7.000000 70.797112 -0.028745
|
||||
7.100000 71.813049 -0.028627
|
||||
7.200000 72.795403 -0.028186
|
||||
7.300000 73.802956 -0.028823
|
||||
7.400000 74.793701 -0.028599
|
||||
7.500000 75.776054 -0.028958
|
||||
7.600000 76.800392 -0.029110
|
||||
7.700000 77.791145 -0.029458
|
||||
7.800000 78.781891 -0.029195
|
||||
7.900000 79.797829 -0.029496
|
||||
8.000000 80.796974 -0.029733
|
||||
8.100000 81.796127 -0.029937
|
||||
8.200000 82.795273 -0.029791
|
||||
8.300000 83.777634 -0.029210
|
||||
8.400000 84.801971 -0.029492
|
||||
8.500000 85.792717 -0.029031
|
||||
8.600000 86.775070 -0.029057
|
||||
8.700000 87.816200 -0.029236
|
||||
8.800000 88.790161 -0.028967
|
||||
8.900000 89.772514 -0.029038
|
||||
9.000000 90.796852 -0.028881
|
||||
9.100000 91.795998 -0.029188
|
||||
9.200000 92.803543 -0.029207
|
||||
9.300000 93.760704 -0.029310
|
||||
9.400000 94.810234 -0.029037
|
||||
9.500000 95.784195 -0.029098
|
||||
9.600000 96.800133 -0.029167
|
||||
9.700000 97.807678 -0.029221
|
||||
9.800000 98.790031 -0.029132
|
||||
9.900000 99.797577 -0.028941
|
||||
10.000000 100.796722 -0.029081
|
||||
10.100000 101.804268 -0.029024
|
||||
10.200000 102.803413 -0.028922
|
||||
10.300000 103.802567 -0.028957
|
||||
10.400000 104.801712 -0.028903
|
||||
10.500000 105.800858 -0.029129
|
||||
10.600000 106.783211 -0.029139
|
||||
10.700000 107.807549 -0.029148
|
||||
10.800000 108.815094 -0.029216
|
||||
10.900000 109.805840 -0.028921
|
||||
11.000000 110.813385 -0.028742
|
||||
11.100000 111.804138 -0.028889
|
||||
11.200000 112.769699 -0.028884
|
||||
11.300000 113.802429 -0.028933
|
||||
11.400000 114.809982 -0.028740
|
||||
11.500000 115.809128 -0.028485
|
||||
11.600000 116.816666 -0.029093
|
||||
11.700000 117.807419 -0.028899
|
||||
11.800000 118.814964 -0.029117
|
||||
11.900000 119.797318 -0.029162
|
||||
12.000000 120.788063 -0.029299
|
||||
12.100000 121.787216 -0.029617
|
||||
12.200000 122.794762 -0.029564
|
||||
12.300000 123.785507 -0.029263
|
||||
12.400000 124.809845 -0.028942
|
||||
12.500000 125.800598 -0.029249
|
||||
12.600000 126.799744 -0.029054
|
||||
12.700000 127.815689 -0.029312
|
||||
12.800000 128.823242 -0.029264
|
||||
12.900000 129.813980 -0.029597
|
||||
13.000000 130.813126 -0.029762
|
||||
13.100000 131.803879 -0.029987
|
||||
13.200000 132.803024 -0.029859
|
||||
13.300000 133.793777 -0.029861
|
||||
13.400000 134.792923 -0.029838
|
||||
13.500000 135.808868 -0.029612
|
||||
13.600000 136.791214 -0.029442
|
||||
13.700000 137.790359 -0.029729
|
||||
13.800000 138.806320 -0.029585
|
||||
13.900000 139.805466 -0.029730
|
||||
14.000000 140.829788 -0.029783
|
||||
14.100000 141.812134 -0.029521
|
||||
14.200000 142.802902 -0.029773
|
||||
14.300000 143.802048 -0.029522
|
||||
14.400000 144.801193 -0.029461
|
||||
14.500000 145.775146 -0.029776
|
||||
14.600000 146.816284 -0.029446
|
||||
14.700000 147.807022 -0.029666
|
||||
14.800000 148.822968 -0.029733
|
||||
14.900000 149.805328 -0.029659
|
||||
15.000000 150.796066 -0.029416
|
||||
15.100000 151.812012 -0.029498
|
||||
15.200000 152.819565 -0.029405
|
||||
15.300000 153.801910 -0.029533
|
||||
15.400000 154.809464 -0.029788
|
||||
15.500000 155.817001 -0.029578
|
||||
15.600000 156.790955 -0.029868
|
||||
15.700000 157.815308 -0.029453
|
||||
15.800000 158.814453 -0.029587
|
||||
15.900000 159.821991 -0.029697
|
||||
16.000000 160.795944 -0.029583
|
||||
16.100000 161.795090 -0.029541
|
||||
16.200001 162.794235 -0.029603
|
||||
16.299999 163.826965 -0.029526
|
||||
16.400000 164.800934 -0.029500
|
||||
16.500000 165.833664 -0.029578
|
||||
16.600000 166.824417 -0.029656
|
||||
16.700001 167.798370 -0.029669
|
||||
16.799999 168.814316 -0.029662
|
||||
16.900000 169.805069 -0.029525
|
||||
17.000000 170.812607 -0.029632
|
||||
17.100000 171.803375 -0.029763
|
||||
17.200001 172.827698 -0.029450
|
||||
17.299999 173.818451 -0.029243
|
||||
17.400000 174.809189 -0.029499
|
||||
17.500000 175.799957 -0.029474
|
||||
17.600000 176.799103 -0.029973
|
||||
17.700001 177.823441 -0.029679
|
||||
17.799999 178.805786 -0.029621
|
||||
17.900000 179.813339 -0.029742
|
||||
18.000000 180.829285 -0.029791
|
||||
18.100000 181.786438 -0.029615
|
||||
18.200001 182.810776 -0.029776
|
||||
18.299999 183.826721 -0.029699
|
||||
18.400000 184.809067 -0.029858
|
||||
18.500000 185.799820 -0.029694
|
||||
18.600000 186.782181 -0.029660
|
||||
18.700001 187.814911 -0.029505
|
||||
18.799999 188.822464 -0.029578
|
||||
18.900000 189.813202 -0.029569
|
||||
19.000000 190.812363 -0.029655
|
||||
19.100000 191.811508 -0.029725
|
||||
19.200001 192.810654 -0.029956
|
||||
19.299999 193.809799 -0.029307
|
||||
19.400000 194.808945 -0.029470
|
||||
19.500000 195.799698 -0.029415
|
||||
19.600000 196.807236 -0.029424
|
||||
19.700001 197.856766 -0.029606
|
||||
19.799999 198.813934 -0.029029
|
||||
89
unilabos/devices/donghua_ec/exports/计时电位法/2号机(计时电位法).txt
Normal file
89
unilabos/devices/donghua_ec/exports/计时电位法/2号机(计时电位法).txt
Normal file
@@ -0,0 +1,89 @@
|
||||
Time(s) E(mV) I(mA)
|
||||
0.100000 1100.313599 0.099854
|
||||
0.200000 1100.313599 0.099846
|
||||
0.300000 1100.313599 0.099829
|
||||
0.400000 1100.313599 0.099829
|
||||
0.500000 1100.313599 0.099821
|
||||
0.600000 1100.313599 0.099829
|
||||
0.700000 1100.313599 0.099846
|
||||
0.800000 1100.313599 0.099812
|
||||
0.900000 1100.313599 0.099812
|
||||
1.000000 1100.313599 0.099829
|
||||
1.100000 1100.313599 0.099812
|
||||
1.200000 1100.313599 0.099804
|
||||
1.300000 1100.313599 0.099796
|
||||
1.400000 1100.313599 0.099804
|
||||
1.500000 1100.313599 0.099804
|
||||
1.600000 1100.313599 0.099787
|
||||
1.700000 1100.313599 0.099796
|
||||
1.800000 1100.313599 0.099796
|
||||
1.900000 1100.313599 0.099787
|
||||
2.000000 1100.313599 0.099821
|
||||
2.100000 1100.313599 0.099821
|
||||
2.200000 1100.313599 0.099812
|
||||
2.300000 1100.313599 0.099829
|
||||
2.400000 1100.313599 0.099829
|
||||
2.500000 1100.313599 0.099838
|
||||
2.600000 1100.313599 0.099854
|
||||
2.700000 1100.313599 0.099863
|
||||
2.800000 1100.313599 0.099854
|
||||
2.900000 1100.313599 0.099846
|
||||
3.000000 1100.313599 0.099863
|
||||
3.100000 1100.313599 0.099871
|
||||
3.200000 1100.313599 0.099879
|
||||
3.300000 1100.313599 0.099888
|
||||
3.400000 1100.313599 0.099896
|
||||
3.500000 1100.313599 0.099913
|
||||
3.600000 1100.313599 0.099930
|
||||
3.700000 1100.313599 0.099938
|
||||
3.800000 1100.313599 0.099930
|
||||
3.900000 1100.313599 0.099947
|
||||
4.000000 1100.313599 0.099963
|
||||
4.100000 1100.313599 0.099947
|
||||
4.200000 1100.313599 0.099980
|
||||
4.300000 1100.313599 0.099997
|
||||
4.400000 1100.313599 0.099972
|
||||
4.500000 1100.313599 0.099947
|
||||
4.600000 1100.313599 0.099947
|
||||
4.700000 1100.313599 0.099955
|
||||
4.800000 1100.313599 0.099955
|
||||
4.900000 1100.313599 0.099989
|
||||
5.000000 1100.313599 0.099997
|
||||
5.100000 1100.313599 0.099980
|
||||
5.200000 1100.313599 0.099989
|
||||
5.300000 1100.313599 0.099963
|
||||
5.400000 1100.313599 0.099980
|
||||
5.500000 1100.313599 0.099989
|
||||
5.600000 1100.313599 0.100005
|
||||
5.700000 1100.313599 0.099963
|
||||
5.800000 1100.313599 0.099980
|
||||
5.900000 1100.313599 0.099980
|
||||
6.000000 1100.313599 0.099980
|
||||
6.100000 1100.313599 0.099972
|
||||
6.200000 1100.313599 0.099997
|
||||
6.300000 1100.313599 0.099972
|
||||
6.400000 1100.313599 0.099955
|
||||
6.500000 1100.313599 0.099972
|
||||
6.600000 1100.313599 0.099963
|
||||
6.700000 1100.313599 0.099980
|
||||
6.800000 1100.313599 0.099963
|
||||
6.900000 1100.313599 0.099955
|
||||
7.000000 1100.313599 0.099989
|
||||
7.100000 1100.313599 0.099989
|
||||
7.200000 1100.313599 0.099980
|
||||
7.300000 1100.313599 0.099980
|
||||
7.400000 1100.313599 0.099980
|
||||
7.500000 1100.313599 0.099963
|
||||
7.600000 1100.313599 0.099980
|
||||
7.700000 1100.313599 0.099955
|
||||
7.800000 1100.313599 0.099980
|
||||
7.900000 1100.313599 0.099963
|
||||
8.000000 1100.313599 0.099989
|
||||
8.100000 1100.313599 0.099980
|
||||
8.200000 1100.313599 0.099989
|
||||
8.300000 1100.313599 0.099989
|
||||
8.400000 1100.313599 0.099972
|
||||
8.500000 1100.313599 0.100005
|
||||
8.600000 1100.313599 0.099980
|
||||
8.700000 1100.313599 0.100005
|
||||
8.800000 1100.313599 0.099997
|
||||
49
unilabos/devices/donghua_ec/exports/阻抗/2号机().txt
Normal file
49
unilabos/devices/donghua_ec/exports/阻抗/2号机().txt
Normal file
@@ -0,0 +1,49 @@
|
||||
Time(s) Zre(Ohm) Zim(Ohm) Z(Ohm) Freq(Hz) Phase(deg) EDC(mV)
|
||||
0.128000 6755.462891 3996.161133 7848.922363 10000.000000 -30.606230 0.000000
|
||||
0.272000 6824.638672 3994.455811 7907.677734 8888.900391 -30.340406 0.000000
|
||||
0.436571 7059.030762 4099.418457 8163.035156 7777.799805 -30.145187 0.000000
|
||||
0.628570 7516.110840 4371.320312 8694.846680 6666.700195 -30.181999 0.000000
|
||||
Time(s) Zre(Ohm) Zim(Ohm) Z(Ohm) Freq(Hz) Phase(deg) EDC(mV)
|
||||
0.128000 4004.078857 1725.499268 4360.045410 10000.000000 -23.312920 0.000000
|
||||
0.272000 3751.894043 1407.421021 4007.186523 8888.900391 -20.562214 0.000000
|
||||
0.436571 3795.918457 1347.885742 4028.125244 7777.799805 -19.549349 0.000000
|
||||
0.628570 3988.254883 1406.353516 4228.948730 6666.700195 -19.423862 0.000000
|
||||
0.858968 4340.244629 1560.648071 4612.303711 5555.600098 -19.777370 0.000000
|
||||
1.146964 4953.287598 1795.551514 5268.687012 4444.500000 -19.925425 0.000000
|
||||
1.530957 5583.770020 2229.389648 6012.375977 3333.399902 -21.764971 0.000000
|
||||
2.106936 6545.107422 2367.948486 6960.288086 2222.300049 -19.889572 0.000000
|
||||
3.258844 7567.829590 1800.241089 7779.004395 1111.199951 -13.380868 0.000000
|
||||
23.258844 7841.500488 363.667908 7849.928711 0.100000 -2.655323 0.000000
|
||||
Time(s) Zre(Ohm) Zim(Ohm) Z(Ohm) Freq(Hz) Phase(deg) EDC(mV)
|
||||
0.128000 4585.909668 1983.974854 4996.671387 10000.000000 -23.394470 0.000000
|
||||
0.272000 4468.743652 1882.592163 4849.105469 8888.900391 -22.844717 0.000000
|
||||
0.436571 4585.427246 1862.162964 4949.120605 7777.799805 -22.102295 0.000000
|
||||
0.628570 4762.757812 1835.767822 5104.302734 6666.700195 -21.078768 0.000000
|
||||
0.858968 5040.085449 1868.267578 5375.210449 5555.600098 -20.338821 0.000000
|
||||
1.146964 5369.222168 2009.491211 5732.939941 4444.500000 -20.518923 0.000000
|
||||
1.530957 5790.665039 2128.829590 6169.580078 3333.399902 -20.185007 0.000000
|
||||
2.106936 6104.018066 1879.055054 6386.695801 2222.300049 -17.110409 0.000000
|
||||
3.258844 6228.028809 1268.160278 6355.830078 1111.199951 -11.509306 0.000000
|
||||
23.258844 6003.355957 413.043579 6017.548340 0.100000 -3.935868 0.000000
|
||||
Time(s) Zre(Ohm) Zim(Ohm) Z(Ohm) Freq(Hz) Phase(deg) EDC(mV)
|
||||
0.128000 3572.804688 1110.373413 3741.371826 10000.000000 -17.264450 0.000000
|
||||
0.272000 3733.689209 1029.676392 3873.069580 8888.900391 -15.417786 0.000000
|
||||
0.436571 3814.144043 1035.226807 3952.137207 7777.799805 -15.185266 0.000000
|
||||
0.628570 3763.235596 993.421204 3892.149414 6666.700195 -14.787639 0.000000
|
||||
0.858968 3746.752197 922.539612 3858.656738 5555.600098 -13.832431 0.000000
|
||||
1.146964 3835.638184 876.370972 3934.481689 4444.500000 -12.870085 0.000000
|
||||
1.530957 3879.458496 808.279602 3962.765869 3333.399902 -11.769126 0.000000
|
||||
2.106936 4037.450439 756.480225 4107.708496 2222.300049 -10.612228 0.000000
|
||||
3.258844 4497.190430 741.943848 4557.982422 1111.199951 -9.368237 0.000000
|
||||
23.258844 5525.440430 743.580078 5575.249023 0.100000 -7.664470 0.000000
|
||||
Time(s) Zre(Ohm) Zim(Ohm) Z(Ohm) Freq(Hz) Phase(deg) EDC(mV)
|
||||
0.128000 4508.749023 1480.736694 4745.671387 10000.000000 -18.180910 0.000000
|
||||
0.272000 4432.497070 1333.723267 4628.806152 8888.900391 -16.746363 0.000000
|
||||
0.436571 4782.034668 1541.624512 5024.386719 7777.799805 -17.868198 0.000000
|
||||
0.628570 5205.424805 1835.522095 5519.563965 6666.700195 -19.423525 0.000000
|
||||
0.858968 5297.061035 1830.112549 5604.298828 5555.600098 -19.059818 0.000000
|
||||
1.146964 5442.899414 1763.836304 5721.562012 4444.500000 -17.955534 0.000000
|
||||
1.530957 5554.852051 1871.875732 5861.766113 3333.399902 -18.622803 0.000000
|
||||
2.106936 5667.162598 1750.656982 5931.402344 2222.300049 -17.166569 0.000000
|
||||
3.258844 5834.244629 1325.616577 5982.948242 1111.199951 -12.801015 0.000000
|
||||
23.258844 5939.145508 584.428345 5967.831055 0.100000 -5.619970 0.000000
|
||||
@@ -0,0 +1,55 @@
|
||||
Time(s) Zre(Ω) Zim(Ω) Z(Ω) Freq(Hz) Phase(°) EDC(V)
|
||||
0.128000 8836.014648 4088.676270 9736.140625 10000.000000 -24.831327 0.000000
|
||||
0.289142 8947.045898 3906.468262 9762.690430 7943.282227 -23.587093 0.000000
|
||||
0.492009 9445.424805 4166.807129 10323.678711 6309.573242 -23.804461 0.000000
|
||||
0.747402 10144.289062 4494.853516 11095.508789 5011.872070 -23.897749 0.000000
|
||||
1.068924 10604.609375 4514.041992 11525.376953 3981.071533 -23.057899 0.000000
|
||||
1.473696 11142.434570 4558.653809 12038.902344 3162.277588 -22.250721 0.000000
|
||||
1.983273 11883.416016 4958.137695 12876.284180 2511.886230 -22.647444 0.000000
|
||||
2.624793 12497.970703 5309.595215 13579.067383 1995.262329 -23.017561 0.000000
|
||||
3.432418 13203.150391 5649.524414 14361.069336 1584.893188 -23.165701 0.000000
|
||||
4.449158 14128.726562 6126.426270 15399.805664 1258.925293 -23.442274 0.000000
|
||||
4.481158 14911.369141 6479.161133 16258.181641 1000.000000 -23.485498 0.000000
|
||||
4.521444 15656.686523 6829.202637 17081.271484 794.328247 -23.566080 0.000000
|
||||
4.572160 16835.632812 7643.918945 18489.673828 630.957336 -24.419577 0.000000
|
||||
4.636009 18222.216797 8657.716797 20174.371094 501.187225 -25.413269 0.000000
|
||||
4.716389 22157.533203 11829.420898 25117.552734 398.107178 -28.096781 0.000000
|
||||
4.817582 23357.978516 11746.929688 26145.468750 316.227753 -26.698219 0.000000
|
||||
4.944976 24984.910156 12806.191406 28075.687500 251.188629 -27.137728 0.000000
|
||||
4.716389 20656.267578 10043.393555 22968.482422 398.107178 -25.929729 0.000000
|
||||
4.817582 23355.941406 11758.861328 26149.011719 316.227753 -26.723581 0.000000
|
||||
4.944976 24946.281250 12709.506836 27997.294922 251.188629 -26.997705 0.000000
|
||||
5.105356 25699.732422 12844.353516 28730.708984 199.526230 -26.555218 0.000000
|
||||
5.307262 28007.763672 13781.557617 31214.837891 158.489319 -26.200048 0.000000
|
||||
5.561448 32278.408203 15294.345703 35718.519531 125.892532 -25.352806 0.000000
|
||||
5.601448 36631.953125 16377.120117 40126.175781 100.000000 -24.088081 0.000000
|
||||
5.651804 36212.464844 14175.981445 38888.316406 79.432823 -21.378698 0.000000
|
||||
5.715200 31005.900391 10025.936523 32586.580078 63.095734 -17.918913 0.000000
|
||||
5.795011 27789.210938 7751.479004 28850.054688 50.118721 -15.585830 0.000000
|
||||
5.895486 35634.070312 9420.209961 36858.207031 39.810719 -14.807939 0.000000
|
||||
6.021977 42739.617188 10889.406250 44105.035156 31.622778 -14.293976 0.000000
|
||||
6.181220 41334.429688 12303.883789 43126.796875 25.118862 -16.576517 0.000000
|
||||
6.381695 44635.375000 13761.085938 46708.503906 19.952623 -17.134529 0.000000
|
||||
6.634078 47430.500000 9581.280273 48388.566406 15.848931 -11.420444 0.000000
|
||||
6.951809 47006.734375 6501.482910 47454.214844 12.589253 -7.874598 0.000000
|
||||
7.351809 47260.820312 5934.597168 47631.968750 10.000000 -7.157238 0.000000
|
||||
7.855379 51975.855469 140.047073 51976.042969 7.943282 -0.154381 0.000000
|
||||
8.489336 58372.769531 2538.466309 58427.937500 6.309573 -2.490062 0.000000
|
||||
9.287441 59285.113281 8795.745117 59934.046875 5.011872 -8.439040 0.000000
|
||||
10.292196 55917.351562 9934.631836 56793.019531 3.981072 -10.074409 0.000000
|
||||
11.557107 51669.527344 1365.285522 51687.562500 3.162278 -1.513598 0.000000
|
||||
13.149536 nan nan nan 2.511886 2.248806 0.000000
|
||||
15.154285 nan nan nan 1.995262 4.537589 0.000000
|
||||
17.678116 nan nan nan 1.584893 1.901186 0.000000
|
||||
20.855431 nan nan nan 1.258925 -14.520571 0.000000
|
||||
22.855431 nan nan nan 1.000000 -21.347967 0.000000
|
||||
25.373281 nan nan nan 0.794328 -16.286789 0.000000
|
||||
28.543070 43693.843750 2706.736816 43777.601562 0.630957 -3.544817 0.000000
|
||||
32.533596 34982.421875 -4359.896973 35253.062500 0.501187 7.104203 0.000000
|
||||
37.557373 30128.955078 -116.846642 30129.181641 0.398107 0.222204 0.000000
|
||||
43.881924 32771.234375 6115.649902 33336.992188 0.316228 -10.570740 0.000000
|
||||
51.844055 18206.365234 -7035.584961 19518.482422 0.251189 21.128300 0.000000
|
||||
61.867813 -1157.350342 -5457.016602 5578.395020 0.199526 101.974136 0.000000
|
||||
74.486984 -7572.944336 -5757.060547 9512.792969 0.158489 142.757385 0.000000
|
||||
90.373489 -6719.222656 -12092.010742 13833.461914 0.125893 119.059814 0.000000
|
||||
110.373489 2759.366699 -3812.787109 4706.532715 0.100000 54.106220 0.000000
|
||||
@@ -0,0 +1,5 @@
|
||||
Time(s) Zre(Ohm) Zim(Ohm) Z(Ohm) Freq(Hz) Phase(deg) EDC(mV)
|
||||
0.128000 5301.714355 1606.345947 5539.722168 10000.000000 -16.856081 0.000000
|
||||
0.272000 5303.290527 1594.772095 5537.886719 8888.900391 -16.736752 0.000000
|
||||
0.436571 5347.800781 1595.693970 5580.789551 7777.799805 -16.614220 0.000000
|
||||
0.628570 5478.396973 1635.483032 5717.310547 6666.700195 -16.622107 0.000000
|
||||
70
unilabos/devices/donghua_ec/init_output.txt
Normal file
70
unilabos/devices/donghua_ec/init_output.txt
Normal file
@@ -0,0 +1,70 @@
|
||||
initialize: True
|
||||
get_machine_ids: {'success': True, 'machine_ids': [1, 3, 2]}
|
||||
version: reflect_linear_scan_signature
|
||||
linear_scan_related_methods:
|
||||
Add_ConstantPotential:['System.String', 'System.Single', 'System.Int32', 'System.Boolean', 'System.Boolean', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['parentTag', 'Voltage', 'VoltageVSType', 'IsAccrueQ', 'isVoltageTrig', 'VoltageOrCurrentTrigDirection', 'VoltageOrCurrentTrigValue', 'capacityTrigDirection', 'capacityTrigValue', 'timePerPoint', 'continueTime', 'IsUseResolution', 'Resolution', 'isUseDeltaI', 'deltaI', 'isUseDeltaQ', 'deltaQ', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'machineId']
|
||||
Start_Circle_Voltammetry_Multi:['System.Boolean', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Boolean', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.Single'] | names=['IsUseInitialPotential', 'InitialPotential', 'InitialPotentialVSType', 'TopPotential1', 'TopPotential1VSType', 'TopPotential2', 'TopPotential2VSType', 'IsUseFinallyPotential', 'FinallyPotential', 'FinallyPotentialVSType', 'ScanRate', 'cycleCount', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId', 'delayTime']
|
||||
Start_Staircase_Cyclic_Voltammetry_MultipleCycles:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['InitialPotential', 'InitialPotentialVSType', 'TopPotential1', 'TopPotential1VSType', 'TopPotential2', 'TopPotential2VSType', 'FinallyPotential', 'FinallyPotentialVSType', 'StepHeight', 'StepTime', 'cycleCount', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_Linear_Scan_Voltammetry:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.Single'] | names=['InitialPotential', 'InitialPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'ScanRate', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId', 'delayTime']
|
||||
Start_Linear_Scan_Voltammetry_New:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.Single'] | names=['InitialPotential', 'InitialPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'ScanRate', 'voltageInterval', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId', 'delayTime']
|
||||
Start_Circle_Voltammetry_Single:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.Single'] | names=['InitialPotential', 'InitialPotentialVSType', 'TopPotential', 'TopPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'ScanRate', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId', 'delayTime']
|
||||
Start_Staircase_Linear_Scan:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['InitialPotential', 'InitialPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'StepHeight', 'StepTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_Potentiodynamic:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['InitialPotential', 'InitialPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'StepHeight', 'StepTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_Staircase_Cyclic_Voltammetry_Single:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['InitialPotential', 'InitialPotentialVSType', 'TopPotential', 'TopPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'StepHeight', 'StepTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_ChronoamperonetryParam:['System.Single', 'System.Single', 'System.Single', 'System.Int32', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['timePerPoint', 'continueTime', 'InitialPotential', 'InitialPotentialVSType', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_ChronopotentiometryParam:['System.Single', 'System.Single', 'System.Single', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['timePerPoint', 'continueTime', 'current', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_ChronocoulometryParam:['System.Single', 'System.Single', 'System.Single', 'System.Int32', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['timePerPoint', 'continueTime', 'potential', 'potentialotentialVSType', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_ConstantPotential:['System.Single', 'System.Int32', 'System.Boolean', 'System.Boolean', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['Voltage', 'VoltageVSType', 'IsAccrueQ', 'isVoltageTrig', 'VoltageOrCurrentTrigDirection', 'VoltageOrCurrentTrigValue', 'capacityTrigDirection', 'capacityTrigValue', 'timePerPoint', 'continueTime', 'IsUseResolution', 'Resolution', 'isUseDeltaI', 'deltaI', 'isUseDeltaQ', 'deltaQ', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'machineId']
|
||||
Start_Potentiostatic_Staircase:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['OutputValue1', 'vsType1', 'OutputValue2', 'vsType2', 'OutputValue3', 'vsType3', 'timePerPoint', 'stepCount', 'measureDelay', 'plusTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_Fast_Potential_Pulses:['System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['plusCount', 'OutputValue1', 'ContinueTime1', 'vsType1', 'OutputValue2', 'ContinueTime2', 'vsType2', 'OutputValue3', 'ContinueTime3', 'vsType3', 'OutputValue4', 'ContinueTime4', 'vsType4', 'OutputValue5', 'ContinueTime5', 'vsType5', 'timePerPoint', 'cycleCount', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
StartConstantPotential_LineScan:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['InitialPotential', 'InitialPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'StaticTime', 'CycleCount', 'TimePerPoint', 'ContinueTime', 'StepHeight', 'SweptRate', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_Circle_Voltammetry_Multi_New:['System.Single', 'System.Boolean', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Boolean', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.Single'] | names=['voltageInterval', 'IsUseInitialPotential', 'InitialPotential', 'InitialPotentialVSType', 'TopPotential1', 'TopPotential1VSType', 'TopPotential2', 'TopPotential2VSType', 'IsUseFinallyPotential', 'FinallyPotential', 'FinallyPotentialVSType', 'ScanRate', 'cycleCount', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId', 'delayTime']
|
||||
Start_Staircase_Cyclic_Voltammetry_MultipleCycles_New:['System.Boolean', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Boolean', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.Single'] | names=['IsUseInitialPotential', 'InitialPotential', 'InitialPotentialVSType', 'TopPotential1', 'TopPotential1VSType', 'TopPotential2', 'TopPotential2VSType', 'IsUseFinallyPotential', 'FinallyPotential', 'FinallyPotentialVSType', 'stepHeight', 'stepTime', 'cycleCount', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId', 'delayTime']
|
||||
all_start_methods:
|
||||
Start_Circle_Voltammetry_Multi:['System.Boolean', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Boolean', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.Single'] | names=['IsUseInitialPotential', 'InitialPotential', 'InitialPotentialVSType', 'TopPotential1', 'TopPotential1VSType', 'TopPotential2', 'TopPotential2VSType', 'IsUseFinallyPotential', 'FinallyPotential', 'FinallyPotentialVSType', 'ScanRate', 'cycleCount', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId', 'delayTime']
|
||||
Start_Staircase_Cyclic_Voltammetry_MultipleCycles:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['InitialPotential', 'InitialPotentialVSType', 'TopPotential1', 'TopPotential1VSType', 'TopPotential2', 'TopPotential2VSType', 'FinallyPotential', 'FinallyPotentialVSType', 'StepHeight', 'StepTime', 'cycleCount', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_SplitLPR:['System.Single', 'System.Single', 'ECCore.VSType', 'System.Single', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['aNodeVoltage', 'cathodeVoltage', 'aNodeVSTye', 'stepHeight', 'stepTime', 'continueTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_OpenCircuit:['System.Single', 'System.Single', 'System.Int32'] | names=['timePerPoint', 'continueTime', 'machineId']
|
||||
Start_Galvanic_Corrosion:['System.Single', 'System.Single', 'System.Int32'] | names=['timePerPoint', 'continueTime', 'machineId']
|
||||
Start_Electrochemical_Noise_En:['System.Single', 'System.Single', 'System.Int32', 'System.Int32'] | names=['timePerPoint', 'timePerSegment', 'segmentCount', 'machineId']
|
||||
Start_Linear_Scan_Voltammetry:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.Single'] | names=['InitialPotential', 'InitialPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'ScanRate', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId', 'delayTime']
|
||||
Start_Linear_Scan_Voltammetry_New:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.Single'] | names=['InitialPotential', 'InitialPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'ScanRate', 'voltageInterval', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId', 'delayTime']
|
||||
Start_Circle_Voltammetry_Single:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.Single'] | names=['InitialPotential', 'InitialPotentialVSType', 'TopPotential', 'TopPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'ScanRate', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId', 'delayTime']
|
||||
Start_Staircase_Linear_Scan:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['InitialPotential', 'InitialPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'StepHeight', 'StepTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_Potentiodynamic:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['InitialPotential', 'InitialPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'StepHeight', 'StepTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_Galvanodynamic:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['InitialPotential', 'InitialPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'StepHeight', 'StepTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_Staircase_Cyclic_Voltammetry_Single:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['InitialPotential', 'InitialPotentialVSType', 'TopPotential', 'TopPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'StepHeight', 'StepTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_Cyclic_Polarization:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.String', 'System.String', 'System.Int32'] | names=['InitialPotential', 'InitialPotentialVSType', 'TopPotential', 'TopPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'StepHeight', 'StepTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'voltageTrigValue', 'currentTrigValue', 'machineId']
|
||||
Start_ChronoamperonetryParam:['System.Single', 'System.Single', 'System.Single', 'System.Int32', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['timePerPoint', 'continueTime', 'InitialPotential', 'InitialPotentialVSType', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_ChronopotentiometryParam:['System.Single', 'System.Single', 'System.Single', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['timePerPoint', 'continueTime', 'current', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_ChronocoulometryParam:['System.Single', 'System.Single', 'System.Single', 'System.Int32', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['timePerPoint', 'continueTime', 'potential', 'potentialotentialVSType', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_SquareWare:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['initialPotential', 'initialPotentialVSType', 'finallyPotential', 'finallyPotentialVSType', 'stepHeight', 'pluseHeight', 'startFreq', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_DifferentialPulse:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['initialPotential', 'initialPotentialVSType', 'finallyPotential', 'finallyPotentialVSType', 'stepHeight', 'pluseHeight', 'pluseWidth', 'stepTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_NormalPulse:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['initialPotential', 'initialPotentialVSType', 'finallyPotential', 'finallyPotentialVSType', 'stepHeight', 'pluseWidth', 'stepTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_EIS:['System.Boolean', 'System.Double', 'System.Double', 'System.Single', 'System.Int32', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.Single', 'System.Int32'] | names=['isVolEIS', 'StartFreq', 'EndFreq', 'Amplitude', 'IntervalType', 'PointCount', 'Voltage', 'VoltageVSType', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId', 'delayTime', 'dataQuality']
|
||||
Start_EnergyOpenCircuit:['System.Boolean', 'System.Single', 'System.Boolean', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['isUseExcursionRate', 'excursionRate', 'isVoltageTrig', 'VoltageOrCurrentTrigDirection', 'VoltageOrCurrentTrigValue', 'capacityTrigDirection', 'capacityTrigValue', 'timePerPoint', 'continueTime', 'IsUseResolution', 'Resolution', 'isUseDeltaI', 'deltaI', 'isUseDeltaQ', 'deltaQ', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'machineId']
|
||||
Start_ConstantPotential:['System.Single', 'System.Int32', 'System.Boolean', 'System.Boolean', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['Voltage', 'VoltageVSType', 'IsAccrueQ', 'isVoltageTrig', 'VoltageOrCurrentTrigDirection', 'VoltageOrCurrentTrigValue', 'capacityTrigDirection', 'capacityTrigValue', 'timePerPoint', 'continueTime', 'IsUseResolution', 'Resolution', 'isUseDeltaI', 'deltaI', 'isUseDeltaQ', 'deltaQ', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'machineId']
|
||||
Start_ConstantContent:['System.Single', 'System.Boolean', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['current', 'isVoltageTrig', 'VoltageOrCurrentTrigDirection', 'VoltageOrCurrentTrigValue', 'capacityTrigDirection', 'capacityTrigValue', 'timePerPoint', 'continueTime', 'IsUseResolution', 'Resolution', 'isUseDeltaI', 'deltaI', 'isUseDeltaQ', 'deltaQ', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'machineId']
|
||||
Start_CurrentChargeDisCharge:['System.Single', 'System.Single', 'System.Single', 'System.Single', 'System.Single', 'System.Single', 'System.Single', 'System.Int32', 'ECCore.SwithPriority', 'ECCore.TimeType', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['CurrentValue1', 'TimeLimitValue1', 'Vol1LimitValue1', 'CurrentValue2', 'TimeLimitValue2', 'Vol1LimitValue2', 'timePerPoint', 'CycleCount', 'CurrentSwithPriority', 'timeType', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'machineId']
|
||||
Start_ConstantPower:['System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['power', 'isCharge', 'currentUpLimit', 'isVoltageTrig', 'VoltageOrCurrentTrigDirection', 'VoltageOrCurrentTrigValue', 'capacityTrigDirection', 'capacityTrigValue', 'timePerPoint', 'continueTime', 'IsUseResolution', 'Resolution', 'isUseDeltaI', 'deltaI', 'isUseDeltaQ', 'deltaQ', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'machineId']
|
||||
Start_ConstantResistance:['System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['resistance', 'isCharge', 'currentUpLimit', 'isVoltageTrig', 'VoltageOrCurrentTrigDirection', 'VoltageOrCurrentTrigValue', 'capacityTrigDirection', 'capacityTrigValue', 'timePerPoint', 'continueTime', 'IsUseResolution', 'Resolution', 'isUseDeltaI', 'deltaI', 'isUseDeltaQ', 'deltaQ', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'machineId']
|
||||
Start_Tafel:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['InitialPotential', 'InitialPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'StepHeight', 'StepTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_LPR:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['InitialPotential', 'InitialPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'StepHeight', 'StepTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_Zra:['System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['timePerPoint', 'continueTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_Potentiostatic_Staircase:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['OutputValue1', 'vsType1', 'OutputValue2', 'vsType2', 'OutputValue3', 'vsType3', 'timePerPoint', 'stepCount', 'measureDelay', 'plusTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_Fast_Potential_Pulses:['System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['plusCount', 'OutputValue1', 'ContinueTime1', 'vsType1', 'OutputValue2', 'ContinueTime2', 'vsType2', 'OutputValue3', 'ContinueTime3', 'vsType3', 'OutputValue4', 'ContinueTime4', 'vsType4', 'OutputValue5', 'ContinueTime5', 'vsType5', 'timePerPoint', 'cycleCount', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_Fast_Galvanic_Pulses:['System.Int32', 'System.Single', 'System.Single', 'System.Single', 'System.Single', 'System.Single', 'System.Single', 'System.Single', 'System.Single', 'System.Single', 'System.Single', 'System.Single', 'System.Int32', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['plusCount', 'OutputValue1', 'ContinueTime1', 'OutputValue2', 'ContinueTime2', 'OutputValue3', 'ContinueTime3', 'OutputValue4', 'ContinueTime4', 'OutputValue5', 'ContinueTime5', 'timePerPoint', 'cycleCount', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_Circle_Voltammetry_Multi_New:['System.Single', 'System.Boolean', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Boolean', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.Single'] | names=['voltageInterval', 'IsUseInitialPotential', 'InitialPotential', 'InitialPotentialVSType', 'TopPotential1', 'TopPotential1VSType', 'TopPotential2', 'TopPotential2VSType', 'IsUseFinallyPotential', 'FinallyPotential', 'FinallyPotentialVSType', 'ScanRate', 'cycleCount', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId', 'delayTime']
|
||||
Start_Staircase_Cyclic_Voltammetry_MultipleCycles_New:['System.Boolean', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Boolean', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.Single'] | names=['IsUseInitialPotential', 'InitialPotential', 'InitialPotentialVSType', 'TopPotential1', 'TopPotential1VSType', 'TopPotential2', 'TopPotential2VSType', 'IsUseFinallyPotential', 'FinallyPotential', 'FinallyPotentialVSType', 'stepHeight', 'stepTime', 'cycleCount', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId', 'delayTime']
|
||||
Start_OpenCircuit_All:['System.Single', 'System.Single'] | names=['timePerPoint', 'continueTime']
|
||||
Start_EIS_All:['System.Boolean', 'System.Double', 'System.Double', 'System.Single', 'System.Int32', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Single', 'System.Int32'] | names=['isVolEIS', 'StartFreq', 'EndFreq', 'Amplitude', 'IntervalType', 'PointCount', 'Voltage', 'VoltageVSType', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'delayTime', 'dataQuality']
|
||||
version: lsv_v1
|
||||
version: lsv_v1
|
||||
start_cv_params: {'is_use_initial_potential': True, 'initial_potential': -1.0, 'initial_potential_vs_type': 0, 'top_potential1': 1.0, 'top_potential1_vs_type': 0, 'top_potential2': -2.0, 'top_potential2_vs_type': 0, 'is_use_finally_potential': True, 'finally_potential': -1.0, 'finally_potential_vs_type': 0, 'scan_rate': 0.2, 'cycle_count': 2, 'is_voltage_rand_auto': 1, 'voltage_rand': '1000', 'is_current_rand_auto': 1, 'current_rand': '1000', 'is_voltage_filter_auto': 1, 'voltage_filter': '10Hz', 'is_current_filter_auto': 1, 'current_filter': '10Hz', 'machine_id': 2}
|
||||
start_cv: {'success': True, 'return_info': 'ok', 'machine_id': 2}
|
||||
start_realtime_output_cv: {'success': True, 'running': True, 'machine_id': 2, 'file': 'D:\\Uni-Lab-OS\\Uni-Lab-OS\\unilabos\\devices\\donghua_ec\\x64release\\DHInterface\\SourceData\\2025-12-12\\循环伏安\\2号机(循环伏安(多循环)).txt'}
|
||||
result_types: [0]
|
||||
data_details: [{'cand': 0, 'len': 0, 'd3': (0, 0, 0), 'd4': (0, 0, 0, 0), 'd7': (0, 0, 0, 0, 0, 0, 0)}]
|
||||
stop_realtime_output_cv: {'success': True, 'machine_id': 2, 'file': 'D:\\Uni-Lab-OS\\Uni-Lab-OS\\unilabos\\devices\\donghua_ec\\x64release\\DHInterface\\SourceData\\2025-12-12\\循环伏安\\2号机(循环伏安(多循环)).txt'}
|
||||
export_cyclic_voltammetry: {'success': True, 'files': ['D:\\Uni-Lab-OS\\Uni-Lab-OS\\unilabos\\devices\\donghua_ec\\exports\\循环伏安\\2号机(循环伏安(多循环)).txt'], 'dest': 'D:\\Uni-Lab-OS\\Uni-Lab-OS\\unilabos\\devices\\donghua_ec\\exports\\循环伏安'}
|
||||
stop_experiment_cv: {'success': True, 'machine_id': 2}
|
||||
176
unilabos/devices/donghua_ec/test_init.py
Normal file
176
unilabos/devices/donghua_ec/test_init.py
Normal file
@@ -0,0 +1,176 @@
|
||||
import os
|
||||
import sys
|
||||
import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
# Ensure repository is on PYTHONPATH
|
||||
repo_root = Path(__file__).resolve().parents[3]
|
||||
sys.path.append(str(repo_root))
|
||||
|
||||
from unilabos.devices.donghua_ec.donghua_ec import DonghuaEC
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
async def main():
|
||||
iface_dir = Path(__file__).resolve().parent / "x64release" / "DHInterface"
|
||||
drv = DonghuaEC(config={"interface_dir": str(iface_dir)})
|
||||
ok = await drv.initialize()
|
||||
res = drv.get_machine_ids()
|
||||
import time as _t
|
||||
out = Path(__file__).resolve().parent / "init_output.txt"
|
||||
try:
|
||||
if out.exists():
|
||||
out.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
with open(out, "w", encoding="utf-8") as f:
|
||||
f.write(f"initialize: {ok}\n")
|
||||
f.write(f"get_machine_ids: {res}\n")
|
||||
f.write("version: reflect_linear_scan_signature\n")
|
||||
try:
|
||||
import System
|
||||
elec = drv._elec
|
||||
t = elec.GetType()
|
||||
sig_lines = []
|
||||
for m in t.GetMethods():
|
||||
name = m.Name
|
||||
if ("Linear" in name) or ("Scan" in name) or ("Volt" in name) or ("Chrono" in name) or ("Potent" in name):
|
||||
ps_types = [str(p.ParameterType.FullName) for p in m.GetParameters()]
|
||||
ps_names = [str(p.Name) for p in m.GetParameters()]
|
||||
sig_lines.append(name + ":" + str(ps_types) + " | names=" + str(ps_names))
|
||||
f.write("linear_scan_related_methods:\n")
|
||||
for ln in sig_lines:
|
||||
f.write(ln + "\n")
|
||||
# dump all Start_* signatures for debugging
|
||||
all_start = []
|
||||
for m in t.GetMethods():
|
||||
name = m.Name
|
||||
if name.startswith("Start_"):
|
||||
ps_types = [str(p.ParameterType.FullName) for p in m.GetParameters()]
|
||||
ps_names = [str(p.Name) for p in m.GetParameters()]
|
||||
all_start.append(name + ":" + str(ps_types) + " | names=" + str(ps_names))
|
||||
f.write("all_start_methods:\n")
|
||||
for ln in all_start:
|
||||
f.write(ln + "\n")
|
||||
except Exception as e:
|
||||
f.write(f"reflect_error: {e}\n")
|
||||
f.write("version: lsv_v1\n")
|
||||
with open(out, "a", encoding="utf-8") as f:
|
||||
f.write("version: lsv_v1\n")
|
||||
# Cyclic Voltammetry (Multi)
|
||||
cv_params = dict(
|
||||
is_use_initial_potential=True,
|
||||
initial_potential=-1.0,
|
||||
initial_potential_vs_type=0,
|
||||
top_potential1=1.0,
|
||||
top_potential1_vs_type=0,
|
||||
top_potential2=-2.0,
|
||||
top_potential2_vs_type=0,
|
||||
is_use_finally_potential=True,
|
||||
finally_potential=-1.0,
|
||||
finally_potential_vs_type=0,
|
||||
scan_rate=0.2,
|
||||
cycle_count=2,
|
||||
is_voltage_rand_auto=1,
|
||||
voltage_rand="1000",
|
||||
is_current_rand_auto=1,
|
||||
current_rand="1000",
|
||||
is_voltage_filter_auto=1,
|
||||
voltage_filter="10Hz",
|
||||
is_current_filter_auto=1,
|
||||
current_filter="10Hz",
|
||||
machine_id=2,
|
||||
)
|
||||
with open(out, "a", encoding="utf-8") as f:
|
||||
f.write(f"start_cv_params: {cv_params}\n")
|
||||
try:
|
||||
start_cv = drv.start_cyclic_voltammetry_multi(**cv_params)
|
||||
except Exception as e:
|
||||
start_cv = {"success": False, "return_info": str(e), "machine_id": 2}
|
||||
try:
|
||||
if not start_cv.get("success"):
|
||||
drv.data["last_result_type"] = "cyclic_voltammetry"
|
||||
except Exception:
|
||||
pass
|
||||
with open(out, "a", encoding="utf-8") as f:
|
||||
f.write(f"start_cv: {start_cv}\n")
|
||||
try:
|
||||
rt4 = drv.start_realtime_output(machine_id=2, interval=0.5)
|
||||
except Exception as e:
|
||||
rt4 = {"success": False, "error": str(e)}
|
||||
with open(out, "a", encoding="utf-8") as f:
|
||||
f.write(f"start_realtime_output_cv: {rt4}\n")
|
||||
try:
|
||||
import System
|
||||
elec = drv._elec
|
||||
rts = []
|
||||
try:
|
||||
rts = list(elec.GetResultDataType(2))
|
||||
except Exception:
|
||||
rts = []
|
||||
with open(out, "a", encoding="utf-8") as f:
|
||||
f.write(f"result_types: {rts}\n")
|
||||
print("result_types:", rts)
|
||||
details = []
|
||||
for cand in rts:
|
||||
try:
|
||||
s = elec.GetData(cand, 2)
|
||||
except Exception:
|
||||
s = None
|
||||
d = {"cand": cand, "len": 0, "d3": (), "d4": (), "d7": ()}
|
||||
try:
|
||||
it = list(s)
|
||||
d["len"] = len(it)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
a0 = list(elec.SplitData(s, 3, 0)); a1 = list(elec.SplitData(s, 3, 1)); a2 = list(elec.SplitData(s, 3, 2))
|
||||
d["d3"] = (len(a0), len(a1), len(a2))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
b0 = list(elec.SplitData(s, 4, 0)); b1 = list(elec.SplitData(s, 4, 1)); b2 = list(elec.SplitData(s, 4, 2)); b3 = list(elec.SplitData(s, 4, 3))
|
||||
d["d4"] = (len(b0), len(b1), len(b2), len(b3))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
c0 = list(elec.SplitData(s, 7, 0)); c1 = list(elec.SplitData(s, 7, 1)); c2 = list(elec.SplitData(s, 7, 2)); c3 = list(elec.SplitData(s, 7, 3)); c4 = list(elec.SplitData(s, 7, 4)); c5 = list(elec.SplitData(s, 7, 5)); c6 = list(elec.SplitData(s, 7, 6))
|
||||
d["d7"] = (len(c0), len(c1), len(c2), len(c3), len(c4), len(c5), len(c6))
|
||||
except Exception:
|
||||
pass
|
||||
details.append(d)
|
||||
with open(out, "a", encoding="utf-8") as f:
|
||||
f.write(f"data_details: {details}\n")
|
||||
print("data_details:", details)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
import time as _t
|
||||
_t.sleep(10.0)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
stop_rt4 = drv.stop_realtime_output(machine_id=2)
|
||||
except Exception as e:
|
||||
stop_rt4 = {"success": False, "error": str(e)}
|
||||
with open(out, "a", encoding="utf-8") as f:
|
||||
f.write(f"stop_realtime_output_cv: {stop_rt4}\n")
|
||||
try:
|
||||
exp_cv = drv.export_cyclic_voltammetry_data(machine_id=2, dest_dir=str(Path(__file__).resolve().parent / "exports" / "循环伏安"))
|
||||
except Exception as e:
|
||||
exp_cv = {"success": False, "error": str(e)}
|
||||
with open(out, "a", encoding="utf-8") as f:
|
||||
f.write(f"export_cyclic_voltammetry: {exp_cv}\n")
|
||||
try:
|
||||
stop_cv = drv.stop_experiment(machine_id=2)
|
||||
except Exception as e:
|
||||
stop_cv = {"success": False, "error": str(e)}
|
||||
with open(out, "a", encoding="utf-8") as f:
|
||||
f.write(f"stop_experiment_cv: {stop_cv}\n")
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except Exception as e:
|
||||
print("error:", e)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user