mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-05-23 06:33:17 +00:00
Compare commits
155 Commits
fix/workst
...
workstatio
| 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 | ||
|
|
957fb41a6f | ||
|
|
26271bcab8 | ||
|
|
84a8223173 | ||
|
|
e8d1263488 | ||
|
|
380b39100d | ||
|
|
56eb7e2ab4 | ||
|
|
23ce145f74 | ||
|
|
b0da149252 | ||
|
|
07c9e6f0fe | ||
|
|
ccec6b9d77 | ||
|
|
dadfdf3d8d | ||
|
|
400bb073d4 | ||
|
|
3f63c36505 | ||
|
|
0ae94f7f3c | ||
|
|
7eacae6442 | ||
|
|
f7d2cb4b9e | ||
|
|
bf980d7248 | ||
|
|
27c0544bfc | ||
|
|
d48e77c9ae | ||
|
|
e70a5bea66 | ||
|
|
467d75dc03 | ||
|
|
9feeb0c430 | ||
|
|
b2f26ffb28 | ||
|
|
4b0d1553e9 | ||
|
|
67ddee2ab2 | ||
|
|
1bcdad9448 | ||
|
|
039c96fe01 | ||
|
|
e1555d10a0 | ||
|
|
f2a96b2041 | ||
|
|
329349639e | ||
|
|
e4cc111523 | ||
|
|
d245ceef1b | ||
|
|
6db7fbd721 | ||
|
|
ab05b858e1 | ||
|
|
43e4c71a8e | ||
|
|
2cf58ca452 | ||
|
|
fd73bb7dcb | ||
|
|
a02cecfd18 | ||
|
|
d6accc3f1c | ||
|
|
39dc443399 | ||
|
|
37b1fca962 | ||
|
|
216f19fb62 | ||
|
|
ec7ca6a1fe |
62
.conda/base/recipe.yaml
Normal file
62
.conda/base/recipe.yaml
Normal file
@@ -0,0 +1,62 @@
|
||||
# unilabos: Production package (depends on unilabos-env + pip unilabos)
|
||||
# For production deployment
|
||||
|
||||
package:
|
||||
name: unilabos
|
||||
version: 0.11.2
|
||||
|
||||
source:
|
||||
path: ../../unilabos
|
||||
target_directory: unilabos
|
||||
|
||||
build:
|
||||
python:
|
||||
entry_points:
|
||||
- unilab = unilabos.app.main:main
|
||||
script:
|
||||
- set PIP_NO_INDEX=
|
||||
- if: win
|
||||
then:
|
||||
- copy %RECIPE_DIR%\..\..\MANIFEST.in %SRC_DIR%
|
||||
- copy %RECIPE_DIR%\..\..\setup.cfg %SRC_DIR%
|
||||
- copy %RECIPE_DIR%\..\..\setup.py %SRC_DIR%
|
||||
- pip install %SRC_DIR%
|
||||
- if: unix
|
||||
then:
|
||||
- cp $RECIPE_DIR/../../MANIFEST.in $SRC_DIR
|
||||
- cp $RECIPE_DIR/../../setup.cfg $SRC_DIR
|
||||
- cp $RECIPE_DIR/../../setup.py $SRC_DIR
|
||||
- pip install $SRC_DIR
|
||||
|
||||
requirements:
|
||||
host:
|
||||
- python ==3.11.14
|
||||
- pip
|
||||
- setuptools
|
||||
- zstd
|
||||
- zstandard
|
||||
run:
|
||||
- zstd
|
||||
- zstandard
|
||||
- networkx
|
||||
- typing_extensions
|
||||
- websockets
|
||||
- pint
|
||||
- fastapi
|
||||
- jinja2
|
||||
- requests
|
||||
- uvicorn
|
||||
- if: not osx
|
||||
then:
|
||||
- opcua
|
||||
- pyserial
|
||||
- pandas
|
||||
- pymodbus
|
||||
- matplotlib
|
||||
- pylibftdi
|
||||
- uni-lab::unilabos-env ==0.11.2
|
||||
|
||||
about:
|
||||
repository: https://github.com/deepmodeling/Uni-Lab-OS
|
||||
license: GPL-3.0-only
|
||||
description: "UniLabOS - Production package with minimal ROS2 dependencies"
|
||||
39
.conda/environment/recipe.yaml
Normal file
39
.conda/environment/recipe.yaml
Normal file
@@ -0,0 +1,39 @@
|
||||
# unilabos-env: conda environment dependencies (ROS2 + conda packages)
|
||||
|
||||
package:
|
||||
name: unilabos-env
|
||||
version: 0.11.2
|
||||
|
||||
build:
|
||||
noarch: generic
|
||||
|
||||
requirements:
|
||||
run:
|
||||
# Python
|
||||
- zstd
|
||||
- zstandard
|
||||
- conda-forge::python ==3.11.14
|
||||
- conda-forge::opencv
|
||||
# ROS2 dependencies (from ci-check.yml)
|
||||
- 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
|
||||
- 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
|
||||
- conda-forge::uv
|
||||
|
||||
# UniLabOS custom messages
|
||||
- uni-lab::ros-humble-unilabos-msgs
|
||||
|
||||
about:
|
||||
repository: https://github.com/deepmodeling/Uni-Lab-OS
|
||||
license: GPL-3.0-only
|
||||
description: "UniLabOS Environment - ROS2 and conda dependencies"
|
||||
42
.conda/full/recipe.yaml
Normal file
42
.conda/full/recipe.yaml
Normal file
@@ -0,0 +1,42 @@
|
||||
# unilabos-full: Full package with all features
|
||||
# Depends on unilabos + complete ROS2 desktop + dev tools
|
||||
|
||||
package:
|
||||
name: unilabos-full
|
||||
version: 0.11.2
|
||||
|
||||
build:
|
||||
noarch: generic
|
||||
|
||||
requirements:
|
||||
run:
|
||||
# Base unilabos package (includes unilabos-env)
|
||||
- uni-lab::unilabos ==0.11.2
|
||||
# Documentation tools
|
||||
- sphinx
|
||||
- sphinx_rtd_theme
|
||||
# Web UI
|
||||
- gradio
|
||||
- flask
|
||||
# Interactive development
|
||||
- ipython
|
||||
- jupyter
|
||||
- jupyros
|
||||
- colcon-common-extensions
|
||||
# ROS2 full desktop (includes rviz2, gazebo, etc.)
|
||||
- robostack-staging::ros-humble-desktop-full
|
||||
# Navigation and motion control
|
||||
- ros-humble-navigation2
|
||||
- ros-humble-ros2-control
|
||||
- ros-humble-robot-state-publisher
|
||||
- ros-humble-joint-state-publisher
|
||||
# MoveIt motion planning
|
||||
- ros-humble-moveit
|
||||
- ros-humble-moveit-servo
|
||||
# Simulation
|
||||
- ros-humble-simulation
|
||||
|
||||
about:
|
||||
repository: https://github.com/deepmodeling/Uni-Lab-OS
|
||||
license: GPL-3.0-only
|
||||
description: "UniLabOS Full - Complete package with ROS2 Desktop, MoveIt, Navigation2, Gazebo, Jupyter"
|
||||
@@ -1,91 +0,0 @@
|
||||
package:
|
||||
name: unilabos
|
||||
version: 0.10.15
|
||||
|
||||
source:
|
||||
path: ../unilabos
|
||||
target_directory: unilabos
|
||||
|
||||
build:
|
||||
python:
|
||||
entry_points:
|
||||
- unilab = unilabos.app.main:main
|
||||
script:
|
||||
- set PIP_NO_INDEX=
|
||||
- if: win
|
||||
then:
|
||||
- copy %RECIPE_DIR%\..\MANIFEST.in %SRC_DIR%
|
||||
- copy %RECIPE_DIR%\..\setup.cfg %SRC_DIR%
|
||||
- copy %RECIPE_DIR%\..\setup.py %SRC_DIR%
|
||||
- call %PYTHON% -m pip install %SRC_DIR%
|
||||
- if: unix
|
||||
then:
|
||||
- cp $RECIPE_DIR/../MANIFEST.in $SRC_DIR
|
||||
- cp $RECIPE_DIR/../setup.cfg $SRC_DIR
|
||||
- cp $RECIPE_DIR/../setup.py $SRC_DIR
|
||||
- $PYTHON -m pip install $SRC_DIR
|
||||
|
||||
requirements:
|
||||
host:
|
||||
- python ==3.11.11
|
||||
- pip
|
||||
- setuptools
|
||||
- zstd
|
||||
- zstandard
|
||||
run:
|
||||
- conda-forge::python ==3.11.11
|
||||
- compilers
|
||||
- cmake
|
||||
- zstd
|
||||
- zstandard
|
||||
- ninja
|
||||
- if: unix
|
||||
then:
|
||||
- make
|
||||
- sphinx
|
||||
- sphinx_rtd_theme
|
||||
- numpy
|
||||
- scipy
|
||||
- pandas
|
||||
- networkx
|
||||
- matplotlib
|
||||
- pint
|
||||
- pyserial
|
||||
- pyusb
|
||||
- pylibftdi
|
||||
- pymodbus
|
||||
- python-can
|
||||
- pyvisa
|
||||
- opencv
|
||||
- pydantic
|
||||
- fastapi
|
||||
- uvicorn
|
||||
- gradio
|
||||
- flask
|
||||
- websockets
|
||||
- ipython
|
||||
- jupyter
|
||||
- jupyros
|
||||
- colcon-common-extensions
|
||||
- robostack-staging::ros-humble-desktop-full
|
||||
- robostack-staging::ros-humble-control-msgs
|
||||
- robostack-staging::ros-humble-sensor-msgs
|
||||
- robostack-staging::ros-humble-trajectory-msgs
|
||||
- ros-humble-navigation2
|
||||
- ros-humble-ros2-control
|
||||
- ros-humble-robot-state-publisher
|
||||
- ros-humble-joint-state-publisher
|
||||
- ros-humble-rosbridge-server
|
||||
- ros-humble-cv-bridge
|
||||
- ros-humble-tf2
|
||||
- ros-humble-moveit
|
||||
- ros-humble-moveit-servo
|
||||
- ros-humble-simulation
|
||||
- ros-humble-tf-transformations
|
||||
- transforms3d
|
||||
- uni-lab::ros-humble-unilabos-msgs
|
||||
|
||||
about:
|
||||
repository: https://github.com/deepmodeling/Uni-Lab-OS
|
||||
license: GPL-3.0-only
|
||||
description: "Uni-Lab-OS"
|
||||
@@ -1,9 +0,0 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
REM upgrade pip
|
||||
"%PREFIX%\python.exe" -m pip install --upgrade pip
|
||||
|
||||
REM install extra deps
|
||||
"%PREFIX%\python.exe" -m pip install paho-mqtt opentrons_shared_data
|
||||
"%PREFIX%\python.exe" -m pip install git+https://github.com/Xuwznln/pylabrobot.git
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
# make sure pip is available
|
||||
"$PREFIX/bin/python" -m pip install --upgrade pip
|
||||
|
||||
# install extra deps
|
||||
"$PREFIX/bin/python" -m pip install paho-mqtt opentrons_shared_data
|
||||
"$PREFIX/bin/python" -m pip install git+https://github.com/Xuwznln/pylabrobot.git
|
||||
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` 的源位置 |
|
||||
@@ -1,26 +0,0 @@
|
||||
.conda
|
||||
# .github
|
||||
.idea
|
||||
# .vscode
|
||||
output
|
||||
pylabrobot_repo
|
||||
recipes
|
||||
scripts
|
||||
service
|
||||
temp
|
||||
# unilabos/test
|
||||
# unilabos/app/web
|
||||
unilabos/device_mesh
|
||||
unilabos_data
|
||||
unilabos_msgs
|
||||
unilabos.egg-info
|
||||
CONTRIBUTORS
|
||||
# LICENSE
|
||||
MANIFEST.in
|
||||
pyrightconfig.json
|
||||
# README.md
|
||||
# README_zh.md
|
||||
setup.py
|
||||
setup.cfg
|
||||
.gitattrubutes
|
||||
**/__pycache__
|
||||
19
.github/dependabot.yml
vendored
Normal file
19
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
version: 2
|
||||
updates:
|
||||
# GitHub Actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
target-branch: "dev"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
time: "06:00"
|
||||
open-pull-requests-limit: 5
|
||||
reviewers:
|
||||
- "msgcenterpy-team"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "github-actions"
|
||||
commit-message:
|
||||
prefix: "ci"
|
||||
include: "scope"
|
||||
67
.github/workflows/ci-check.yml
vendored
Normal file
67
.github/workflows/ci-check.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
name: CI Check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, dev]
|
||||
pull_request:
|
||||
branches: [main, dev]
|
||||
|
||||
jobs:
|
||||
registry-check:
|
||||
runs-on: windows-latest
|
||||
|
||||
env:
|
||||
# Fix Unicode encoding issue on Windows runner (cp1252 -> utf-8)
|
||||
PYTHONIOENCODING: utf-8
|
||||
PYTHONUTF8: 1
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: cmd
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Miniforge
|
||||
uses: conda-incubator/setup-miniconda@v3
|
||||
with:
|
||||
miniforge-version: latest
|
||||
use-mamba: true
|
||||
channels: robostack-staging,conda-forge,uni-lab
|
||||
channel-priority: flexible
|
||||
activate-environment: check-env
|
||||
auto-update-conda: false
|
||||
show-channel-urls: true
|
||||
|
||||
- name: Install ROS dependencies, uv and unilabos-msgs
|
||||
run: |
|
||||
echo Installing ROS dependencies...
|
||||
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: |
|
||||
call conda activate check-env
|
||||
echo Installing pip dependencies...
|
||||
uv pip install -r unilabos/utils/requirements.txt
|
||||
uv pip install pywinauto git+https://github.com/Xuwznln/pylabrobot.git
|
||||
uv pip uninstall enum34 || echo enum34 not installed, skipping
|
||||
uv pip install .
|
||||
|
||||
- name: Run check mode (AST registry validation)
|
||||
run: |
|
||||
call conda activate check-env
|
||||
echo Running check mode...
|
||||
python -m unilabos --check_mode --skip_env_check
|
||||
|
||||
- name: Check for uncommitted changes
|
||||
shell: bash
|
||||
run: |
|
||||
if ! git diff --exit-code; then
|
||||
echo "::error::检测到文件变化!请先在本地运行 'python -m unilabos --complete_registry' 并提交变更"
|
||||
echo "变化的文件:"
|
||||
git diff --name-only
|
||||
exit 1
|
||||
fi
|
||||
echo "检查通过:无文件变化"
|
||||
100
.github/workflows/conda-pack-build.yml
vendored
100
.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:
|
||||
@@ -13,9 +17,24 @@ on:
|
||||
required: false
|
||||
default: 'win-64'
|
||||
type: string
|
||||
build_full:
|
||||
description: '是否构建完整版 unilabos-full (默认构建轻量版 unilabos)'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
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:
|
||||
@@ -49,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
|
||||
@@ -57,10 +78,10 @@ jobs:
|
||||
echo "should_build=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- 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)
|
||||
@@ -69,8 +90,8 @@ jobs:
|
||||
with:
|
||||
miniforge-version: latest
|
||||
use-mamba: true
|
||||
python-version: '3.11.11'
|
||||
channels: conda-forge,robostack-staging,uni-lab,defaults
|
||||
python-version: '3.11.14'
|
||||
channels: conda-forge,robostack-staging,uni-lab
|
||||
channel-priority: flexible
|
||||
activate-environment: unilab
|
||||
auto-update-conda: false
|
||||
@@ -81,7 +102,14 @@ 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 conda-pack -c uni-lab -c robostack-staging -c conda-forge -y
|
||||
echo Build full: ${{ env.BUILD_FULL }}
|
||||
if "${{ env.BUILD_FULL }}"=="true" (
|
||||
echo Installing unilabos-full ^(complete package^)...
|
||||
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 --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)
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64'
|
||||
@@ -89,7 +117,14 @@ 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 conda-pack -c uni-lab -c robostack-staging -c conda-forge -y
|
||||
echo "Build full: ${{ env.BUILD_FULL }}"
|
||||
if [[ "${{ env.BUILD_FULL }}" == "true" ]]; then
|
||||
echo "Installing unilabos-full (complete package)..."
|
||||
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 --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)
|
||||
if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64'
|
||||
@@ -115,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
|
||||
@@ -146,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
|
||||
@@ -207,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
|
||||
|
||||
@@ -216,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
|
||||
|
||||
@@ -248,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:
|
||||
@@ -284,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:"
|
||||
@@ -293,9 +331,9 @@ jobs:
|
||||
|
||||
- name: Upload distribution package
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
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
|
||||
@@ -307,13 +345,18 @@ jobs:
|
||||
echo Build Summary
|
||||
echo ==========================================
|
||||
echo Platform: ${{ matrix.platform }}
|
||||
echo Branch: ${{ github.event.inputs.branch }}
|
||||
echo Python version: 3.11.11
|
||||
echo Branch: ${{ env.PACKAGE_REF }}
|
||||
echo Python version: 3.11.14
|
||||
if "${{ env.BUILD_FULL }}"=="true" (
|
||||
echo Package: unilabos-full ^(complete^)
|
||||
) else (
|
||||
echo Package: unilabos ^(minimal^)
|
||||
)
|
||||
echo.
|
||||
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
|
||||
@@ -327,13 +370,18 @@ jobs:
|
||||
echo "Build Summary"
|
||||
echo "=========================================="
|
||||
echo "Platform: ${{ matrix.platform }}"
|
||||
echo "Branch: ${{ github.event.inputs.branch }}"
|
||||
echo "Python version: 3.11.11"
|
||||
echo "Branch: ${{ env.PACKAGE_REF }}"
|
||||
echo "Python version: 3.11.14"
|
||||
if [[ "${{ env.BUILD_FULL }}" == "true" ]]; then
|
||||
echo "Package: unilabos-full (complete)"
|
||||
else
|
||||
echo "Package: unilabos (minimal)"
|
||||
fi
|
||||
echo ""
|
||||
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"
|
||||
|
||||
41
.github/workflows/deploy-docs.yml
vendored
41
.github/workflows/deploy-docs.yml
vendored
@@ -1,10 +1,12 @@
|
||||
name: Deploy Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
# 在 CI Check 成功后自动触发(仅 main 分支)
|
||||
workflow_run:
|
||||
workflows: ["CI Check"]
|
||||
types: [completed]
|
||||
branches: [main]
|
||||
# 手动触发
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
@@ -33,12 +35,19 @@ concurrency:
|
||||
jobs:
|
||||
# Build documentation
|
||||
build:
|
||||
# 只在以下情况运行:
|
||||
# 1. workflow_run 触发且 CI Check 成功
|
||||
# 2. 手动触发
|
||||
if: |
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch || github.ref }}
|
||||
# workflow_run 时使用触发工作流的分支,手动触发时使用输入的分支
|
||||
ref: ${{ github.event.workflow_run.head_branch || github.event.inputs.branch || github.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Miniforge (with mamba)
|
||||
@@ -46,8 +55,8 @@ jobs:
|
||||
with:
|
||||
miniforge-version: latest
|
||||
use-mamba: true
|
||||
python-version: '3.11.11'
|
||||
channels: conda-forge,robostack-staging,uni-lab,defaults
|
||||
python-version: '3.11.14'
|
||||
channels: conda-forge,robostack-staging,uni-lab
|
||||
channel-priority: flexible
|
||||
activate-environment: unilab
|
||||
auto-update-conda: false
|
||||
@@ -57,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: |
|
||||
@@ -75,8 +84,10 @@ jobs:
|
||||
|
||||
- name: Setup Pages
|
||||
id: pages
|
||||
uses: actions/configure-pages@v4
|
||||
if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true')
|
||||
uses: actions/configure-pages@v5
|
||||
if: |
|
||||
github.event.workflow_run.head_branch == 'main' ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true')
|
||||
|
||||
- name: Build Sphinx documentation
|
||||
run: |
|
||||
@@ -94,14 +105,18 @@ jobs:
|
||||
test -f docs/_build/html/index.html && echo "✓ index.html exists" || echo "✗ index.html missing"
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true')
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
if: |
|
||||
github.event.workflow_run.head_branch == 'main' ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true')
|
||||
with:
|
||||
path: docs/_build/html
|
||||
|
||||
# Deploy to GitHub Pages
|
||||
deploy:
|
||||
if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true')
|
||||
if: |
|
||||
github.event.workflow_run.head_branch == 'main' ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true')
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
77
.github/workflows/multi-platform-build.yml
vendored
77
.github/workflows/multi-platform-build.yml
vendored
@@ -1,11 +1,19 @@
|
||||
name: Multi-Platform Conda Build
|
||||
|
||||
on:
|
||||
# 在 CI Check 工作流完成后触发(仅限 main/dev 分支)
|
||||
workflow_run:
|
||||
workflows: ["CI Check"]
|
||||
types:
|
||||
- completed
|
||||
branches: [main, dev]
|
||||
# 支持 tag 推送(不依赖 CI Check)
|
||||
push:
|
||||
branches: [main, dev]
|
||||
tags: ['v*']
|
||||
pull_request:
|
||||
branches: [main, dev]
|
||||
# GitHub Release 发布时自动构建并上传
|
||||
release:
|
||||
types: [published]
|
||||
# 手动触发
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
platforms:
|
||||
@@ -17,9 +25,37 @@ on:
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
skip_ci_check:
|
||||
description: '跳过等待 CI Check (手动触发时可选)'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
# 等待 CI Check 完成的 job (仅用于 workflow_run 触发)
|
||||
wait-for-ci:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'workflow_run'
|
||||
outputs:
|
||||
should_continue: ${{ steps.check.outputs.should_continue }}
|
||||
steps:
|
||||
- name: Check CI status
|
||||
id: check
|
||||
run: |
|
||||
if [[ "${{ github.event.workflow_run.conclusion }}" == "success" ]]; then
|
||||
echo "should_continue=true" >> $GITHUB_OUTPUT
|
||||
echo "CI Check passed, proceeding with build"
|
||||
else
|
||||
echo "should_continue=false" >> $GITHUB_OUTPUT
|
||||
echo "CI Check did not succeed (status: ${{ github.event.workflow_run.conclusion }}), skipping build"
|
||||
fi
|
||||
|
||||
build:
|
||||
needs: [wait-for-ci]
|
||||
# 运行条件:workflow_run 触发且 CI 成功,或者其他触发方式
|
||||
if: |
|
||||
always() &&
|
||||
(needs.wait-for-ci.result == 'skipped' || needs.wait-for-ci.outputs.should_continue == 'true')
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -44,8 +80,10 @@ jobs:
|
||||
shell: bash -l {0}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
# 如果是 workflow_run 触发,使用触发 CI Check 的 commit
|
||||
ref: ${{ github.event.workflow_run.head_sha || github.event.release.tag_name || github.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check if platform should be built
|
||||
@@ -61,28 +99,31 @@ 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-activate-base: false
|
||||
auto-update-conda: false
|
||||
show-channel-urls: true
|
||||
|
||||
- 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 }}"
|
||||
|
||||
@@ -90,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
|
||||
@@ -115,7 +156,7 @@ jobs:
|
||||
|
||||
- name: Upload conda package artifacts
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: conda-package-${{ matrix.platform }}
|
||||
path: conda-packages-temp
|
||||
@@ -123,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
|
||||
|
||||
139
.github/workflows/unilabos-conda-build.yml
vendored
139
.github/workflows/unilabos-conda-build.yml
vendored
@@ -1,25 +1,58 @@
|
||||
name: UniLabOS Conda Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, dev]
|
||||
tags: ['v*']
|
||||
pull_request:
|
||||
branches: [main, dev]
|
||||
# 在 Multi-Platform Conda Build 成功上传 msgs 后自动触发
|
||||
workflow_run:
|
||||
workflows: ["Multi-Platform Conda Build"]
|
||||
types: [completed]
|
||||
# 手动触发
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
platforms:
|
||||
description: '选择构建平台 (逗号分隔): linux-64, osx-64, osx-arm64, win-64'
|
||||
required: false
|
||||
default: 'linux-64'
|
||||
build_full:
|
||||
description: '是否构建 unilabos-full 完整包 (默认只构建 unilabos 基础包)'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
upload_to_anaconda:
|
||||
description: '是否上传到Anaconda.org'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
skip_ci_check:
|
||||
description: '跳过等待 CI Check (手动触发时可选)'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
# 等待上游 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 upstream workflow status
|
||||
id: check
|
||||
run: |
|
||||
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 "Multi-Platform Conda Build passed for release/tag, proceeding with UniLabOS build"
|
||||
else
|
||||
echo "should_continue=false" >> $GITHUB_OUTPUT
|
||||
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-upstream]
|
||||
# 运行条件:workflow_run 触发且上游成功,或者手动触发
|
||||
if: |
|
||||
always() &&
|
||||
(needs.wait-for-upstream.result == 'skipped' || needs.wait-for-upstream.outputs.should_continue == 'true')
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -40,8 +73,10 @@ jobs:
|
||||
shell: bash -l {0}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
# 如果是 workflow_run 触发,使用上游 conda 包构建的 commit
|
||||
ref: ${{ github.event.workflow_run.head_sha || github.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check if platform should be built
|
||||
@@ -57,36 +92,100 @@ 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-activate-base: false
|
||||
auto-update-conda: false
|
||||
show-channel-urls: true
|
||||
|
||||
- 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 "Building UniLabOS package"
|
||||
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_name == 'workflow_dispatch' && github.event.inputs.build_full == 'true' }}" == "true" ]]; then
|
||||
echo " - unilabos-full (complete package)"
|
||||
fi
|
||||
|
||||
- name: Build conda package
|
||||
- name: Build unilabos-env (conda environment only, noarch)
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
rattler-build build -r .conda/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge
|
||||
echo "Building unilabos-env (conda environment dependencies)..."
|
||||
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_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
|
||||
conda run -n build-env anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
|
||||
done
|
||||
|
||||
- name: Build unilabos (with pip package)
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
echo "Building unilabos package..."
|
||||
# 如果已上传到 Anaconda,从 uni-lab channel 获取 unilabos-env;否则从本地 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_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
|
||||
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 }}..."
|
||||
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
|
||||
conda run -n build-env anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
|
||||
done
|
||||
|
||||
- name: List built packages
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
@@ -108,17 +207,9 @@ jobs:
|
||||
|
||||
- name: Upload conda package artifacts
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: conda-package-unilabos-${{ matrix.platform }}
|
||||
path: conda-packages-temp
|
||||
if-no-files-found: warn
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload to Anaconda.org (uni-lab organization)
|
||||
if: github.event.inputs.upload_to_anaconda == 'true'
|
||||
run: |
|
||||
for package in $(find ./output -name "*.conda"); do
|
||||
echo "Uploading $package to uni-lab organization..."
|
||||
anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
|
||||
done
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -4,6 +4,8 @@ temp/
|
||||
output/
|
||||
unilabos_data/
|
||||
pyrightconfig.json
|
||||
.cursorignore
|
||||
device_package*/
|
||||
## Python
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
@@ -249,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 个资源全部无法放置到对应仓库位,前端不显示。
|
||||
@@ -1,4 +1,5 @@
|
||||
recursive-include unilabos/test *
|
||||
recursive-include unilabos/utils *
|
||||
recursive-include unilabos/registry *.yaml
|
||||
recursive-include unilabos/app/web/static *
|
||||
recursive-include unilabos/app/web/templates *
|
||||
|
||||
38
README.md
38
README.md
@@ -31,26 +31,46 @@ Detailed documentation can be found at:
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Setup Conda Environment
|
||||
### 1. Setup Conda Environment
|
||||
|
||||
Uni-Lab-OS recommends using `mamba` for environment management:
|
||||
Uni-Lab-OS recommends using `mamba` for environment management. Choose the package that fits your needs:
|
||||
|
||||
| Package | Use Case | Contents |
|
||||
|---------|----------|----------|
|
||||
| `unilabos` | **Recommended for most users** | Complete package, ready to use |
|
||||
| `unilabos-env` | Developers (editable install) | Environment only, install unilabos via pip |
|
||||
| `unilabos-full` | Simulation/Visualization | unilabos + ROS2 Desktop + Gazebo + MoveIt |
|
||||
|
||||
```bash
|
||||
# Create new environment
|
||||
mamba create -n unilab python=3.11.11
|
||||
mamba create -n unilab python=3.11.14
|
||||
mamba activate unilab
|
||||
mamba install -n unilab uni-lab::unilabos -c robostack-staging -c conda-forge
|
||||
|
||||
# Option A: Standard installation (recommended for most users)
|
||||
mamba install uni-lab::unilabos -c robostack-staging -c conda-forge
|
||||
|
||||
# Option B: For developers (editable mode development)
|
||||
mamba install uni-lab::unilabos-env -c robostack-staging -c conda-forge
|
||||
# Then install unilabos and dependencies:
|
||||
git clone https://github.com/deepmodeling/Uni-Lab-OS.git && cd Uni-Lab-OS
|
||||
pip install -e .
|
||||
uv pip install -r unilabos/utils/requirements.txt
|
||||
|
||||
# Option C: Full installation (simulation/visualization)
|
||||
mamba install uni-lab::unilabos-full -c robostack-staging -c conda-forge
|
||||
```
|
||||
|
||||
2. Install Dev Uni-Lab-OS
|
||||
**When to use which?**
|
||||
- **unilabos**: Standard installation for production deployment and general usage (recommended)
|
||||
- **unilabos-env**: For developers who need `pip install -e .` editable mode, modify source code
|
||||
- **unilabos-full**: For simulation (Gazebo), visualization (rviz2), and Jupyter notebooks
|
||||
|
||||
### 2. Clone Repository (Optional, for developers)
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
# Clone the repository (only needed for development or examples)
|
||||
git clone https://github.com/deepmodeling/Uni-Lab-OS.git
|
||||
cd Uni-Lab-OS
|
||||
|
||||
# Install Uni-Lab-OS
|
||||
pip install .
|
||||
```
|
||||
|
||||
3. Start Uni-Lab System
|
||||
|
||||
38
README_zh.md
38
README_zh.md
@@ -31,26 +31,46 @@ Uni-Lab-OS 是一个用于实验室自动化的综合平台,旨在连接和控
|
||||
|
||||
## 快速开始
|
||||
|
||||
1. 配置 Conda 环境
|
||||
### 1. 配置 Conda 环境
|
||||
|
||||
Uni-Lab-OS 建议使用 `mamba` 管理环境。根据您的操作系统选择适当的环境文件:
|
||||
Uni-Lab-OS 建议使用 `mamba` 管理环境。根据您的需求选择合适的安装包:
|
||||
|
||||
| 安装包 | 适用场景 | 包含内容 |
|
||||
|--------|----------|----------|
|
||||
| `unilabos` | **推荐大多数用户** | 完整安装包,开箱即用 |
|
||||
| `unilabos-env` | 开发者(可编辑安装) | 仅环境依赖,通过 pip 安装 unilabos |
|
||||
| `unilabos-full` | 仿真/可视化 | unilabos + ROS2 桌面版 + Gazebo + MoveIt |
|
||||
|
||||
```bash
|
||||
# 创建新环境
|
||||
mamba create -n unilab python=3.11.11
|
||||
mamba create -n unilab python=3.11.14
|
||||
mamba activate unilab
|
||||
mamba install -n unilab uni-lab::unilabos -c robostack-staging -c conda-forge
|
||||
|
||||
# 方案 A:标准安装(推荐大多数用户)
|
||||
mamba install uni-lab::unilabos -c robostack-staging -c conda-forge
|
||||
|
||||
# 方案 B:开发者环境(可编辑模式开发)
|
||||
mamba install uni-lab::unilabos-env -c robostack-staging -c conda-forge
|
||||
# 然后安装 unilabos 和依赖:
|
||||
git clone https://github.com/deepmodeling/Uni-Lab-OS.git && cd Uni-Lab-OS
|
||||
pip install -e .
|
||||
uv pip install -r unilabos/utils/requirements.txt
|
||||
|
||||
# 方案 C:完整安装(仿真/可视化)
|
||||
mamba install uni-lab::unilabos-full -c robostack-staging -c conda-forge
|
||||
```
|
||||
|
||||
2. 安装开发版 Uni-Lab-OS:
|
||||
**如何选择?**
|
||||
- **unilabos**:标准安装,适用于生产部署和日常使用(推荐)
|
||||
- **unilabos-env**:开发者使用,支持 `pip install -e .` 可编辑模式,可修改源代码
|
||||
- **unilabos-full**:需要仿真(Gazebo)、可视化(rviz2)或 Jupyter Notebook
|
||||
|
||||
### 2. 克隆仓库(可选,供开发者使用)
|
||||
|
||||
```bash
|
||||
# 克隆仓库
|
||||
# 克隆仓库(仅开发或查看示例时需要)
|
||||
git clone https://github.com/deepmodeling/Uni-Lab-OS.git
|
||||
cd Uni-Lab-OS
|
||||
|
||||
# 安装 Uni-Lab-OS
|
||||
pip install .
|
||||
```
|
||||
|
||||
3. 启动 Uni-Lab 系统
|
||||
|
||||
@@ -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后查看日志
|
||||
|
||||
@@ -31,6 +31,14 @@
|
||||
|
||||
详细的安装步骤请参考 [安装指南](installation.md)。
|
||||
|
||||
**选择合适的安装包:**
|
||||
|
||||
| 安装包 | 适用场景 | 包含组件 |
|
||||
| --------------- | ---------------------------- | --------------------------------------------- |
|
||||
| `unilabos` | **推荐大多数用户**,生产部署 | 完整安装包,开箱即用 |
|
||||
| `unilabos-env` | 开发者(可编辑安装) | 仅环境依赖,通过 pip 安装 unilabos |
|
||||
| `unilabos-full` | 仿真/可视化 | unilabos + 完整 ROS2 桌面版 + Gazebo + MoveIt |
|
||||
|
||||
**关键步骤:**
|
||||
|
||||
```bash
|
||||
@@ -38,15 +46,31 @@
|
||||
# 下载 Miniforge: https://github.com/conda-forge/miniforge/releases
|
||||
|
||||
# 2. 创建 Conda 环境
|
||||
mamba create -n unilab python=3.11.11
|
||||
mamba create -n unilab python=3.11.14
|
||||
|
||||
# 3. 激活环境
|
||||
mamba activate unilab
|
||||
|
||||
# 4. 安装 Uni-Lab-OS
|
||||
# 4. 安装 Uni-Lab-OS(选择其一)
|
||||
|
||||
# 方案 A:标准安装(推荐大多数用户)
|
||||
mamba install uni-lab::unilabos -c robostack-staging -c conda-forge
|
||||
|
||||
# 方案 B:开发者环境(可编辑模式开发)
|
||||
mamba install uni-lab::unilabos-env -c robostack-staging -c conda-forge
|
||||
pip install -e /path/to/Uni-Lab-OS # 可编辑安装
|
||||
uv pip install -r unilabos/utils/requirements.txt # 安装 pip 依赖
|
||||
|
||||
# 方案 C:完整版(仿真/可视化)
|
||||
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
|
||||
|
||||
#### 1.2 验证安装
|
||||
|
||||
```bash
|
||||
@@ -65,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. 创建新实验室
|
||||
|
||||
@@ -274,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 添加设备和物料
|
||||
|
||||
@@ -283,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
|
||||
|
||||
@@ -416,6 +438,9 @@ unilab --ak your_ak --sk your_sk -g test/experiments/mock_devices/mock_all.json
|
||||
1. 访问 Web 界面,进入"仪器耗材"模块
|
||||
2. 在"仪器设备"区域找到并添加上述设备
|
||||
3. 在"物料耗材"区域找到并添加容器
|
||||
4. 在workstation中配置protocol_type包含PumpTransferProtocol
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
@@ -426,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. 在画布上连接它们(建立父子关系)
|
||||
|
||||

|
||||
|
||||
@@ -768,7 +794,44 @@ Waiting for host service...
|
||||
|
||||
详细的设备驱动编写指南请参考 [添加设备驱动](../developer_guide/add_device.md)。
|
||||
|
||||
#### 9.1 为什么需要自定义设备?
|
||||
#### 9.1 开发环境准备
|
||||
|
||||
**推荐使用 `unilabos-env` + `pip install -e .` + `uv pip install`** 进行设备开发:
|
||||
|
||||
```bash
|
||||
# 1. 创建环境并安装 unilabos-env(ROS2 + conda 依赖 + uv)
|
||||
mamba create -n unilab python=3.11.14
|
||||
conda activate unilab
|
||||
mamba install uni-lab::unilabos-env -c robostack-staging -c conda-forge
|
||||
|
||||
# 2. 克隆代码
|
||||
git clone https://github.com/deepmodeling/Uni-Lab-OS.git
|
||||
cd Uni-Lab-OS
|
||||
|
||||
# 3. 以可编辑模式安装(推荐使用脚本,自动检测中文环境)
|
||||
python scripts/dev_install.py
|
||||
|
||||
# 或手动安装:
|
||||
pip install -e .
|
||||
uv pip install -r unilabos/utils/requirements.txt
|
||||
```
|
||||
|
||||
**为什么使用这种方式?**
|
||||
|
||||
- `unilabos-env` 提供 ROS2 核心组件和 uv(通过 conda 安装,避免编译)
|
||||
- `unilabos/utils/requirements.txt` 包含所有运行时需要的 pip 依赖
|
||||
- `dev_install.py` 自动检测中文环境,中文系统自动使用清华镜像
|
||||
- 使用 `uv` 替代 `pip`,安装速度更快
|
||||
- 可编辑模式:代码修改**立即生效**,无需重新安装
|
||||
|
||||
**如果安装失败或速度太慢**,可以手动执行(使用清华镜像):
|
||||
|
||||
```bash
|
||||
pip install -e . -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
|
||||
uv pip install -r unilabos/utils/requirements.txt -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
|
||||
```
|
||||
|
||||
#### 9.2 为什么需要自定义设备?
|
||||
|
||||
Uni-Lab-OS 内置了常见设备,但您的实验室可能有特殊设备需要集成:
|
||||
|
||||
@@ -777,7 +840,7 @@ Uni-Lab-OS 内置了常见设备,但您的实验室可能有特殊设备需要
|
||||
- 特殊的实验流程
|
||||
- 第三方设备集成
|
||||
|
||||
#### 9.2 创建 Python 包
|
||||
#### 9.3 创建 Python 包
|
||||
|
||||
为了方便开发和管理,建议为您的实验室创建独立的 Python 包。
|
||||
|
||||
@@ -814,7 +877,7 @@ touch my_lab_devices/my_lab_devices/__init__.py
|
||||
touch my_lab_devices/my_lab_devices/devices/__init__.py
|
||||
```
|
||||
|
||||
#### 9.3 创建 setup.py
|
||||
#### 9.4 创建 setup.py
|
||||
|
||||
```python
|
||||
# my_lab_devices/setup.py
|
||||
@@ -845,7 +908,7 @@ setup(
|
||||
)
|
||||
```
|
||||
|
||||
#### 9.4 开发安装
|
||||
#### 9.5 开发安装
|
||||
|
||||
使用 `-e` 参数进行可编辑安装,这样代码修改后立即生效:
|
||||
|
||||
@@ -860,7 +923,7 @@ pip install -e . -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
|
||||
- 方便调试和测试
|
||||
- 支持版本控制(git)
|
||||
|
||||
#### 9.5 编写设备驱动
|
||||
#### 9.6 编写设备驱动
|
||||
|
||||
创建设备驱动文件:
|
||||
|
||||
@@ -1001,7 +1064,7 @@ class MyPump:
|
||||
- **返回 Dict**:所有动作方法返回字典类型
|
||||
- **文档字符串**:详细说明参数和功能
|
||||
|
||||
#### 9.6 测试设备驱动
|
||||
#### 9.7 测试设备驱动
|
||||
|
||||
创建简单的测试脚本:
|
||||
|
||||
@@ -1733,32 +1796,27 @@ unilab --ak your_ak --sk your_sk -g graph.json \
|
||||
**详细步骤:**
|
||||
|
||||
1. **需求分析**:
|
||||
|
||||
- 明确实验流程
|
||||
- 列出所需设备和物料
|
||||
- 设计工作流程图
|
||||
|
||||
2. **环境搭建**:
|
||||
|
||||
- 安装 Uni-Lab-OS
|
||||
- 创建实验室账号
|
||||
- 准备开发工具(IDE、Git)
|
||||
|
||||
3. **原型验证**:
|
||||
|
||||
- 使用虚拟设备测试流程
|
||||
- 验证工作流逻辑
|
||||
- 调整参数
|
||||
|
||||
4. **迭代开发**:
|
||||
|
||||
- 实现自定义设备驱动(同时撰写单点函数测试)
|
||||
- 编写注册表
|
||||
- 单元测试
|
||||
- 集成测试
|
||||
|
||||
5. **测试部署**:
|
||||
|
||||
- 连接真实硬件
|
||||
- 空跑测试
|
||||
- 小规模试验
|
||||
@@ -1808,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. 保存到云端
|
||||
|
||||
BIN
docs/user_guide/image/add_protocol.png
Normal file
BIN
docs/user_guide/image/add_protocol.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 275 KiB After Width: | Height: | Size: 415 KiB |
@@ -13,15 +13,26 @@
|
||||
- 开发者需要 Git 和基本的 Python 开发知识
|
||||
- 自定义 msgs 需要 GitHub 账号
|
||||
|
||||
## 安装包选择
|
||||
|
||||
Uni-Lab-OS 提供三个安装包版本,根据您的需求选择:
|
||||
|
||||
| 安装包 | 适用场景 | 包含组件 | 磁盘占用 |
|
||||
|--------|----------|----------|----------|
|
||||
| **unilabos** | **推荐大多数用户**,生产部署 | 完整安装包,开箱即用 | ~2-3 GB |
|
||||
| **unilabos-env** | 开发者环境(可编辑安装) | 仅环境依赖,通过 pip 安装 unilabos | ~2 GB |
|
||||
| **unilabos-full** | 仿真可视化、完整功能体验 | unilabos + 完整 ROS2 桌面版 + Gazebo + MoveIt | ~8-10 GB |
|
||||
|
||||
## 安装方式选择
|
||||
|
||||
根据您的使用场景,选择合适的安装方式:
|
||||
|
||||
| 安装方式 | 适用人群 | 特点 | 安装时间 |
|
||||
| ---------------------- | -------------------- | ------------------------------ | ---------------------------- |
|
||||
| **方式一:一键安装** | 实验室用户、快速体验 | 预打包环境,离线可用,无需配置 | 5-10 分钟 (网络良好的情况下) |
|
||||
| **方式二:手动安装** | 标准用户、生产环境 | 灵活配置,版本可控 | 10-20 分钟 |
|
||||
| **方式三:开发者安装** | 开发者、需要修改源码 | 可编辑模式,支持自定义 msgs | 20-30 分钟 |
|
||||
| 安装方式 | 适用人群 | 推荐安装包 | 特点 | 安装时间 |
|
||||
| ---------------------- | -------------------- | ----------------- | ------------------------------ | ---------------------------- |
|
||||
| **方式一:一键安装** | 快速体验、演示 | 预打包环境 | 离线可用,无需配置 | 5-10 分钟 (网络良好的情况下) |
|
||||
| **方式二:手动安装** | **大多数用户** | `unilabos` | 完整功能,开箱即用 | 10-20 分钟 |
|
||||
| **方式三:开发者安装** | 开发者、需要修改源码 | `unilabos-env` | 可编辑模式,支持自定义开发 | 20-30 分钟 |
|
||||
| **仿真/可视化** | 仿真测试、可视化调试 | `unilabos-full` | 含 Gazebo、rviz2、MoveIt | 30-60 分钟 |
|
||||
|
||||
---
|
||||
|
||||
@@ -144,17 +155,38 @@ bash Miniforge3-$(uname)-$(uname -m).sh
|
||||
使用以下命令创建 Uni-Lab 专用环境:
|
||||
|
||||
```bash
|
||||
mamba create -n unilab python=3.11.11 # 目前ros2组件依赖版本大多为3.11.11
|
||||
mamba create -n unilab python=3.11.14 # 目前ros2组件依赖版本大多为3.11.14
|
||||
mamba activate unilab
|
||||
mamba install -n unilab uni-lab::unilabos -c robostack-staging -c conda-forge
|
||||
|
||||
# 选择安装包(三选一):
|
||||
|
||||
# 方案 A:标准安装(推荐大多数用户)
|
||||
mamba install uni-lab::unilabos -c robostack-staging -c conda-forge
|
||||
|
||||
# 方案 B:开发者环境(可编辑模式开发)
|
||||
mamba install uni-lab::unilabos-env -c robostack-staging -c conda-forge
|
||||
# 然后安装 unilabos 和 pip 依赖:
|
||||
git clone https://github.com/deepmodeling/Uni-Lab-OS.git && cd Uni-Lab-OS
|
||||
pip install -e .
|
||||
uv pip install -r unilabos/utils/requirements.txt
|
||||
|
||||
# 方案 C:完整版(含仿真和可视化工具)
|
||||
mamba install uni-lab::unilabos-full -c robostack-staging -c conda-forge
|
||||
```
|
||||
|
||||
**参数说明**:
|
||||
|
||||
- `-n unilab`: 创建名为 "unilab" 的环境
|
||||
- `uni-lab::unilabos`: 从 uni-lab channel 安装 unilabos 包
|
||||
- `uni-lab::unilabos`: 安装 unilabos 完整包,开箱即用(推荐)
|
||||
- `uni-lab::unilabos-env`: 仅安装环境依赖,适合开发者使用 `pip install -e .`
|
||||
- `uni-lab::unilabos-full`: 安装完整包(含 ROS2 Desktop、Gazebo、MoveIt 等)
|
||||
- `-c robostack-staging -c conda-forge`: 添加额外的软件源
|
||||
|
||||
**包选择建议**:
|
||||
- **日常使用/生产部署**:安装 `unilabos`(推荐,完整功能,开箱即用)
|
||||
- **开发者**:安装 `unilabos-env`,然后使用 `uv pip install -r unilabos/utils/requirements.txt` 安装依赖,再 `pip install -e .` 进行可编辑安装
|
||||
- **仿真/可视化**:安装 `unilabos-full`(Gazebo、rviz2、MoveIt)
|
||||
|
||||
**如果遇到网络问题**,可以使用清华镜像源加速下载:
|
||||
|
||||
```bash
|
||||
@@ -163,8 +195,14 @@ mamba config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/m
|
||||
mamba config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/
|
||||
mamba config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/
|
||||
|
||||
# 然后重新执行安装命令
|
||||
# 然后重新执行安装命令(推荐标准安装)
|
||||
mamba create -n unilab uni-lab::unilabos -c robostack-staging
|
||||
|
||||
# 或完整版(仿真/可视化)
|
||||
mamba create -n unilab uni-lab::unilabos-full -c robostack-staging
|
||||
|
||||
# pip 安装时使用清华镜像(开发者安装时使用)
|
||||
uv pip install -r unilabos/utils/requirements.txt -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
|
||||
```
|
||||
|
||||
### 第三步:激活环境
|
||||
@@ -203,58 +241,87 @@ cd Uni-Lab-OS
|
||||
cd Uni-Lab-OS
|
||||
```
|
||||
|
||||
### 第二步:安装基础环境
|
||||
### 第二步:安装开发环境(unilabos-env)
|
||||
|
||||
**推荐方式**:先通过**方式一(一键安装)**或**方式二(手动安装)**完成基础环境的安装,这将包含所有必需的依赖项(ROS2、msgs 等)。
|
||||
|
||||
#### 选项 A:通过一键安装(推荐)
|
||||
|
||||
参考上文"方式一:一键安装",完成基础环境的安装后,激活环境:
|
||||
**重要**:开发者请使用 `unilabos-env` 包,它专为开发者设计:
|
||||
- 包含 ROS2 核心组件和消息包(ros-humble-ros-core、std-msgs、geometry-msgs 等)
|
||||
- 包含 transforms3d、cv-bridge、tf2 等 conda 依赖
|
||||
- 包含 `uv` 工具,用于快速安装 pip 依赖
|
||||
- **不包含** pip 依赖和 unilabos 包(由 `pip install -e .` 和 `uv pip install` 安装)
|
||||
|
||||
```bash
|
||||
# 创建并激活环境
|
||||
mamba create -n unilab python=3.11.14
|
||||
conda activate unilab
|
||||
|
||||
# 安装开发者环境包(ROS2 + conda 依赖 + uv)
|
||||
mamba install uni-lab::unilabos-env -c robostack-staging -c conda-forge
|
||||
```
|
||||
|
||||
#### 选项 B:通过手动安装
|
||||
### 第三步:安装 pip 依赖和可编辑模式安装
|
||||
|
||||
参考上文"方式二:手动安装",创建并安装环境:
|
||||
|
||||
```bash
|
||||
mamba create -n unilab python=3.11.11
|
||||
conda activate unilab
|
||||
mamba install -n unilab uni-lab::unilabos -c robostack-staging -c conda-forge
|
||||
```
|
||||
|
||||
**说明**:这会安装包括 Python 3.11.11、ROS2 Humble、ros-humble-unilabos-msgs 和所有必需依赖
|
||||
|
||||
### 第三步:切换到开发版本
|
||||
|
||||
现在你已经有了一个完整可用的 Uni-Lab 环境,接下来将 unilabos 包切换为开发版本:
|
||||
克隆代码并安装依赖:
|
||||
|
||||
```bash
|
||||
# 确保环境已激活
|
||||
conda activate unilab
|
||||
|
||||
# 卸载 pip 安装的 unilabos(保留所有 conda 依赖)
|
||||
pip uninstall unilabos -y
|
||||
|
||||
# 克隆 dev 分支(如果还未克隆)
|
||||
cd /path/to/your/workspace
|
||||
git clone -b dev https://github.com/deepmodeling/Uni-Lab-OS.git
|
||||
# 或者如果已经克隆,切换到 dev 分支
|
||||
# 克隆仓库(如果还未克隆)
|
||||
git clone https://github.com/deepmodeling/Uni-Lab-OS.git
|
||||
cd Uni-Lab-OS
|
||||
|
||||
# 切换到 dev 分支(可选)
|
||||
git checkout dev
|
||||
git pull
|
||||
|
||||
# 以可编辑模式安装开发版 unilabos
|
||||
pip install -e . -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
|
||||
```
|
||||
|
||||
**参数说明**:
|
||||
**推荐:使用安装脚本**(自动检测中文环境,使用 uv 加速):
|
||||
|
||||
- `-e`: editable mode(可编辑模式),代码修改立即生效,无需重新安装
|
||||
- `-i`: 使用清华镜像源加速下载
|
||||
- `pip uninstall unilabos`: 只卸载 pip 安装的 unilabos 包,不影响 conda 安装的其他依赖(如 ROS2、msgs 等)
|
||||
```bash
|
||||
# 自动检测中文环境,如果是中文系统则使用清华镜像
|
||||
python scripts/dev_install.py
|
||||
|
||||
# 或者手动指定:
|
||||
python scripts/dev_install.py --china # 强制使用清华镜像
|
||||
python scripts/dev_install.py --no-mirror # 强制使用 PyPI
|
||||
python scripts/dev_install.py --skip-deps # 跳过 pip 依赖安装
|
||||
python scripts/dev_install.py --use-pip # 使用 pip 而非 uv
|
||||
```
|
||||
|
||||
**手动安装**(如果脚本安装失败或速度太慢):
|
||||
|
||||
```bash
|
||||
# 1. 安装 unilabos(可编辑模式)
|
||||
pip install -e .
|
||||
|
||||
# 2. 使用 uv 安装 pip 依赖(推荐,速度更快)
|
||||
uv pip install -r unilabos/utils/requirements.txt
|
||||
|
||||
# 国内用户使用清华镜像:
|
||||
pip install -e . -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
|
||||
uv pip install -r unilabos/utils/requirements.txt -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
|
||||
```
|
||||
|
||||
**注意**:
|
||||
- `uv` 已包含在 `unilabos-env` 中,无需单独安装
|
||||
- `unilabos/utils/requirements.txt` 包含运行 unilabos 所需的所有 pip 依赖
|
||||
- 部分特殊包(如 pylabrobot)会在运行时由 unilabos 自动检测并安装
|
||||
|
||||
**为什么使用可编辑模式?**
|
||||
|
||||
- `-e` (editable mode):代码修改**立即生效**,无需重新安装
|
||||
- 适合开发调试:修改代码后直接运行测试
|
||||
- 与 `unilabos-env` 配合:环境依赖由 conda 管理,unilabos 代码由 pip 管理
|
||||
|
||||
**验证安装**:
|
||||
|
||||
```bash
|
||||
# 检查 unilabos 版本
|
||||
python -c "import unilabos; print(unilabos.__version__)"
|
||||
|
||||
# 检查安装位置(应该指向你的代码目录)
|
||||
pip show unilabos | grep Location
|
||||
```
|
||||
|
||||
### 第四步:安装或自定义 ros-humble-unilabos-msgs(可选)
|
||||
|
||||
@@ -464,7 +531,45 @@ cd $CONDA_PREFIX/envs/unilab
|
||||
|
||||
### 问题 8: 环境很大,有办法减小吗?
|
||||
|
||||
**解决方案**: 预打包的环境包含所有依赖,通常较大(压缩后 2-5GB)。这是为了确保离线安装和完整功能。如果空间有限,考虑使用方式二手动安装,只安装需要的组件。
|
||||
**解决方案**:
|
||||
|
||||
1. **使用 `unilabos` 标准版**(推荐大多数用户):
|
||||
```bash
|
||||
mamba install uni-lab::unilabos -c robostack-staging -c conda-forge
|
||||
```
|
||||
标准版包含完整功能,环境大小约 2-3GB(相比完整版的 8-10GB)。
|
||||
|
||||
2. **使用 `unilabos-env` 开发者版**(最小化):
|
||||
```bash
|
||||
mamba install uni-lab::unilabos-env -c robostack-staging -c conda-forge
|
||||
# 然后手动安装依赖
|
||||
pip install -e .
|
||||
uv pip install -r unilabos/utils/requirements.txt
|
||||
```
|
||||
开发者版只包含环境依赖,体积最小约 2GB。
|
||||
|
||||
3. **按需安装额外组件**:
|
||||
如果后续需要特定功能,可以单独安装:
|
||||
```bash
|
||||
# 需要 Jupyter
|
||||
mamba install jupyter jupyros
|
||||
|
||||
# 需要可视化
|
||||
mamba install matplotlib opencv
|
||||
|
||||
# 需要仿真(注意:这会安装大量依赖)
|
||||
mamba install ros-humble-gazebo-ros
|
||||
```
|
||||
|
||||
4. **预打包环境问题**:
|
||||
预打包环境(方式一)包含所有依赖,通常较大(压缩后 2-5GB)。这是为了确保离线安装和完整功能。
|
||||
|
||||
**包选择建议**:
|
||||
| 需求 | 推荐包 | 预估大小 |
|
||||
|------|--------|----------|
|
||||
| 日常使用/生产部署 | `unilabos` | ~2-3 GB |
|
||||
| 开发调试(可编辑模式) | `unilabos-env` | ~2 GB |
|
||||
| 仿真/可视化 | `unilabos-full` | ~8-10 GB |
|
||||
|
||||
### 问题 9: 如何更新到最新版本?
|
||||
|
||||
@@ -511,6 +616,7 @@ mamba update ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-f
|
||||
|
||||
**提示**:
|
||||
|
||||
- 生产环境推荐使用方式二(手动安装)的稳定版本
|
||||
- 开发和测试推荐使用方式三(开发者安装)
|
||||
- 快速体验和演示推荐使用方式一(一键安装)
|
||||
- **大多数用户**推荐使用方式二(手动安装)的 `unilabos` 标准版
|
||||
- **开发者**推荐使用方式三(开发者安装),安装 `unilabos-env` 后使用 `uv pip install -r unilabos/utils/requirements.txt` 安装依赖
|
||||
- **仿真/可视化**推荐安装 `unilabos-full` 完整版
|
||||
- **快速体验和演示**推荐使用方式一(一键安装)
|
||||
|
||||
@@ -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.15
|
||||
version: 0.11.2
|
||||
source:
|
||||
path: ../../unilabos_msgs
|
||||
target_directory: src
|
||||
@@ -25,7 +25,7 @@ requirements:
|
||||
build:
|
||||
- ${{ compiler('cxx') }}
|
||||
- ${{ compiler('c') }}
|
||||
- python ==3.11.11
|
||||
- python ==3.11.14
|
||||
- numpy
|
||||
- if: build_platform != target_platform
|
||||
then:
|
||||
@@ -63,14 +63,14 @@ requirements:
|
||||
- robostack-staging::ros-humble-rosidl-default-generators
|
||||
- robostack-staging::ros-humble-std-msgs
|
||||
- robostack-staging::ros-humble-geometry-msgs
|
||||
- robostack-staging::ros2-distro-mutex=0.6
|
||||
- robostack-staging::ros2-distro-mutex=0.7
|
||||
run:
|
||||
- robostack-staging::ros-humble-action-msgs
|
||||
- robostack-staging::ros-humble-ros-workspace
|
||||
- robostack-staging::ros-humble-rosidl-default-runtime
|
||||
- robostack-staging::ros-humble-std-msgs
|
||||
- robostack-staging::ros-humble-geometry-msgs
|
||||
- robostack-staging::ros2-distro-mutex=0.6
|
||||
- robostack-staging::ros2-distro-mutex=0.7
|
||||
- if: osx and x86_64
|
||||
then:
|
||||
- __osx >=${{ MACOSX_DEPLOYMENT_TARGET|default('10.14') }}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package:
|
||||
name: unilabos
|
||||
version: "0.10.15"
|
||||
version: "0.11.2"
|
||||
|
||||
source:
|
||||
path: ../..
|
||||
|
||||
@@ -85,7 +85,7 @@ Verification:
|
||||
-------------
|
||||
|
||||
The verify_installation.py script will check:
|
||||
- Python version (3.11.11)
|
||||
- Python version (3.11.14)
|
||||
- ROS2 rclpy installation
|
||||
- UniLabOS installation and dependencies
|
||||
|
||||
@@ -104,7 +104,7 @@ Build Information:
|
||||
|
||||
Branch: {branch}
|
||||
Platform: {platform}
|
||||
Python: 3.11.11
|
||||
Python: 3.11.14
|
||||
Date: {build_date}
|
||||
|
||||
Troubleshooting:
|
||||
|
||||
214
scripts/dev_install.py
Normal file
214
scripts/dev_install.py
Normal file
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Development installation script for UniLabOS.
|
||||
Auto-detects Chinese locale and uses appropriate mirror.
|
||||
|
||||
Usage:
|
||||
python scripts/dev_install.py
|
||||
python scripts/dev_install.py --no-mirror # Force no mirror
|
||||
python scripts/dev_install.py --china # Force China mirror
|
||||
python scripts/dev_install.py --skip-deps # Skip pip dependencies installation
|
||||
|
||||
Flow:
|
||||
1. pip install -e . (install unilabos in editable mode)
|
||||
2. Detect Chinese locale
|
||||
3. Use uv to install pip dependencies from requirements.txt
|
||||
4. Special packages (like pylabrobot) are handled by environment_check.py at runtime
|
||||
"""
|
||||
|
||||
import locale
|
||||
import subprocess
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
# Tsinghua mirror URL
|
||||
TSINGHUA_MIRROR = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"
|
||||
|
||||
|
||||
def is_chinese_locale() -> bool:
|
||||
"""
|
||||
Detect if system is in Chinese locale.
|
||||
Same logic as EnvironmentChecker._is_chinese_locale()
|
||||
"""
|
||||
try:
|
||||
lang = locale.getdefaultlocale()[0]
|
||||
if lang and ("zh" in lang.lower() or "chinese" in lang.lower()):
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def run_command(cmd: list, description: str, retry: int = 2) -> bool:
|
||||
"""Run command with retry support."""
|
||||
print(f"[INFO] {description}")
|
||||
print(f"[CMD] {' '.join(cmd)}")
|
||||
|
||||
for attempt in range(retry + 1):
|
||||
try:
|
||||
result = subprocess.run(cmd, check=True, timeout=600)
|
||||
print(f"[OK] {description}")
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
if attempt < retry:
|
||||
print(f"[WARN] Attempt {attempt + 1} failed, retrying...")
|
||||
else:
|
||||
print(f"[ERROR] {description} failed: {e}")
|
||||
return False
|
||||
except subprocess.TimeoutExpired:
|
||||
print(f"[ERROR] {description} timed out")
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
def install_editable(project_root: Path, use_mirror: bool) -> bool:
|
||||
"""Install unilabos in editable mode using pip."""
|
||||
cmd = [sys.executable, "-m", "pip", "install", "-e", str(project_root)]
|
||||
if use_mirror:
|
||||
cmd.extend(["-i", TSINGHUA_MIRROR])
|
||||
|
||||
return run_command(cmd, "Installing unilabos in editable mode")
|
||||
|
||||
|
||||
def install_requirements_uv(requirements_file: Path, use_mirror: bool) -> bool:
|
||||
"""Install pip dependencies using uv (installed via conda-forge::uv)."""
|
||||
cmd = ["uv", "pip", "install", "-r", str(requirements_file)]
|
||||
if use_mirror:
|
||||
cmd.extend(["-i", TSINGHUA_MIRROR])
|
||||
|
||||
return run_command(cmd, "Installing pip dependencies with uv", retry=2)
|
||||
|
||||
|
||||
def install_requirements_pip(requirements_file: Path, use_mirror: bool) -> bool:
|
||||
"""Fallback: Install pip dependencies using pip."""
|
||||
cmd = [sys.executable, "-m", "pip", "install", "-r", str(requirements_file)]
|
||||
if use_mirror:
|
||||
cmd.extend(["-i", TSINGHUA_MIRROR])
|
||||
|
||||
return run_command(cmd, "Installing pip dependencies with pip", retry=2)
|
||||
|
||||
|
||||
def check_uv_available() -> bool:
|
||||
"""Check if uv is available (installed via conda-forge::uv)."""
|
||||
try:
|
||||
subprocess.run(["uv", "--version"], capture_output=True, check=True)
|
||||
return True
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Development installation script for UniLabOS")
|
||||
parser.add_argument("--china", action="store_true", help="Force use China mirror (Tsinghua)")
|
||||
parser.add_argument("--no-mirror", action="store_true", help="Force use default PyPI (no mirror)")
|
||||
parser.add_argument(
|
||||
"--skip-deps", action="store_true", help="Skip pip dependencies installation (only install unilabos)"
|
||||
)
|
||||
parser.add_argument("--use-pip", action="store_true", help="Use pip instead of uv for dependencies")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Determine project root
|
||||
script_dir = Path(__file__).parent
|
||||
project_root = script_dir.parent
|
||||
requirements_file = project_root / "unilabos" / "utils" / "requirements.txt"
|
||||
|
||||
if not (project_root / "setup.py").exists():
|
||||
print(f"[ERROR] setup.py not found in {project_root}")
|
||||
sys.exit(1)
|
||||
|
||||
print("=" * 60)
|
||||
print("UniLabOS Development Installation")
|
||||
print("=" * 60)
|
||||
print(f"Project root: {project_root}")
|
||||
print()
|
||||
|
||||
# Determine mirror usage based on locale
|
||||
if args.no_mirror:
|
||||
use_mirror = False
|
||||
print("[INFO] Mirror disabled by --no-mirror flag")
|
||||
elif args.china:
|
||||
use_mirror = True
|
||||
print("[INFO] China mirror enabled by --china flag")
|
||||
else:
|
||||
use_mirror = is_chinese_locale()
|
||||
if use_mirror:
|
||||
print("[INFO] Chinese locale detected, using Tsinghua mirror")
|
||||
else:
|
||||
print("[INFO] Non-Chinese locale detected, using default PyPI")
|
||||
|
||||
print()
|
||||
|
||||
# Step 1: Install unilabos in editable mode
|
||||
print("[STEP 1] Installing unilabos in editable mode...")
|
||||
if not install_editable(project_root, use_mirror):
|
||||
print("[ERROR] Failed to install unilabos")
|
||||
print()
|
||||
print("Manual fallback:")
|
||||
if use_mirror:
|
||||
print(f" pip install -e {project_root} -i {TSINGHUA_MIRROR}")
|
||||
else:
|
||||
print(f" pip install -e {project_root}")
|
||||
sys.exit(1)
|
||||
|
||||
print()
|
||||
|
||||
# Step 2: Install pip dependencies
|
||||
if args.skip_deps:
|
||||
print("[INFO] Skipping pip dependencies installation (--skip-deps)")
|
||||
else:
|
||||
print("[STEP 2] Installing pip dependencies...")
|
||||
|
||||
if not requirements_file.exists():
|
||||
print(f"[WARN] Requirements file not found: {requirements_file}")
|
||||
print("[INFO] Skipping dependencies installation")
|
||||
else:
|
||||
# Try uv first (faster), fallback to pip
|
||||
if args.use_pip:
|
||||
print("[INFO] Using pip (--use-pip flag)")
|
||||
success = install_requirements_pip(requirements_file, use_mirror)
|
||||
elif check_uv_available():
|
||||
print("[INFO] Using uv (installed via conda-forge::uv)")
|
||||
success = install_requirements_uv(requirements_file, use_mirror)
|
||||
if not success:
|
||||
print("[WARN] uv failed, falling back to pip...")
|
||||
success = install_requirements_pip(requirements_file, use_mirror)
|
||||
else:
|
||||
print("[WARN] uv not available (should be installed via: mamba install conda-forge::uv)")
|
||||
print("[INFO] Falling back to pip...")
|
||||
success = install_requirements_pip(requirements_file, use_mirror)
|
||||
|
||||
if not success:
|
||||
print()
|
||||
print("[WARN] Failed to install some dependencies automatically.")
|
||||
print("You can manually install them:")
|
||||
if use_mirror:
|
||||
print(f" uv pip install -r {requirements_file} -i {TSINGHUA_MIRROR}")
|
||||
print(" or:")
|
||||
print(f" pip install -r {requirements_file} -i {TSINGHUA_MIRROR}")
|
||||
else:
|
||||
print(f" uv pip install -r {requirements_file}")
|
||||
print(" or:")
|
||||
print(f" pip install -r {requirements_file}")
|
||||
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("Installation complete!")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print("Note: Some special packages (like pylabrobot) are installed")
|
||||
print("automatically at runtime by unilabos if needed.")
|
||||
print()
|
||||
print("Verify installation:")
|
||||
print(' python -c "import unilabos; print(unilabos.__version__)"')
|
||||
print()
|
||||
print("If you encounter issues, you can manually install dependencies:")
|
||||
if use_mirror:
|
||||
print(f" uv pip install -r unilabos/utils/requirements.txt -i {TSINGHUA_MIRROR}")
|
||||
else:
|
||||
print(" uv pip install -r unilabos/utils/requirements.txt")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
2
setup.py
2
setup.py
@@ -4,7 +4,7 @@ package_name = 'unilabos'
|
||||
|
||||
setup(
|
||||
name=package_name,
|
||||
version='0.10.15',
|
||||
version='0.11.2',
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=['setuptools'],
|
||||
|
||||
213
tests/workflow/test.json
Normal file
213
tests/workflow/test.json
Normal file
@@ -0,0 +1,213 @@
|
||||
{
|
||||
"workflow": [
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"action_args": {
|
||||
"sources": "cell_lines",
|
||||
"targets": "Liquid_1",
|
||||
"asp_vol": 100.0,
|
||||
"dis_vol": 74.75,
|
||||
"asp_flow_rate": 94.0,
|
||||
"dis_flow_rate": 95.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"action_args": {
|
||||
"sources": "cell_lines",
|
||||
"targets": "Liquid_2",
|
||||
"asp_vol": 100.0,
|
||||
"dis_vol": 74.75,
|
||||
"asp_flow_rate": 94.0,
|
||||
"dis_flow_rate": 95.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"action_args": {
|
||||
"sources": "cell_lines",
|
||||
"targets": "Liquid_3",
|
||||
"asp_vol": 100.0,
|
||||
"dis_vol": 74.75,
|
||||
"asp_flow_rate": 94.0,
|
||||
"dis_flow_rate": 95.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"action_args": {
|
||||
"sources": "cell_lines_2",
|
||||
"targets": "Liquid_4",
|
||||
"asp_vol": 100.0,
|
||||
"dis_vol": 74.75,
|
||||
"asp_flow_rate": 94.0,
|
||||
"dis_flow_rate": 95.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"action_args": {
|
||||
"sources": "cell_lines_2",
|
||||
"targets": "Liquid_5",
|
||||
"asp_vol": 100.0,
|
||||
"dis_vol": 74.75,
|
||||
"asp_flow_rate": 94.0,
|
||||
"dis_flow_rate": 95.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"action_args": {
|
||||
"sources": "cell_lines_2",
|
||||
"targets": "Liquid_6",
|
||||
"asp_vol": 100.0,
|
||||
"dis_vol": 74.75,
|
||||
"asp_flow_rate": 94.0,
|
||||
"dis_flow_rate": 95.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"action_args": {
|
||||
"sources": "cell_lines_3",
|
||||
"targets": "dest_set",
|
||||
"asp_vol": 100.0,
|
||||
"dis_vol": 74.75,
|
||||
"asp_flow_rate": 94.0,
|
||||
"dis_flow_rate": 95.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"action_args": {
|
||||
"sources": "cell_lines_3",
|
||||
"targets": "dest_set_2",
|
||||
"asp_vol": 100.0,
|
||||
"dis_vol": 74.75,
|
||||
"asp_flow_rate": 94.0,
|
||||
"dis_flow_rate": 95.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"action_args": {
|
||||
"sources": "cell_lines_3",
|
||||
"targets": "dest_set_3",
|
||||
"asp_vol": 100.0,
|
||||
"dis_vol": 74.75,
|
||||
"asp_flow_rate": 94.0,
|
||||
"dis_flow_rate": 95.5
|
||||
}
|
||||
}
|
||||
],
|
||||
"reagent": {
|
||||
"Liquid_1": {
|
||||
"slot": 1,
|
||||
"well": [
|
||||
"A4",
|
||||
"A7",
|
||||
"A10"
|
||||
],
|
||||
"labware": "rep 1"
|
||||
},
|
||||
"Liquid_4": {
|
||||
"slot": 1,
|
||||
"well": [
|
||||
"A4",
|
||||
"A7",
|
||||
"A10"
|
||||
],
|
||||
"labware": "rep 1"
|
||||
},
|
||||
"dest_set": {
|
||||
"slot": 1,
|
||||
"well": [
|
||||
"A4",
|
||||
"A7",
|
||||
"A10"
|
||||
],
|
||||
"labware": "rep 1"
|
||||
},
|
||||
"Liquid_2": {
|
||||
"slot": 2,
|
||||
"well": [
|
||||
"A3",
|
||||
"A5",
|
||||
"A8"
|
||||
],
|
||||
"labware": "rep 2"
|
||||
},
|
||||
"Liquid_5": {
|
||||
"slot": 2,
|
||||
"well": [
|
||||
"A3",
|
||||
"A5",
|
||||
"A8"
|
||||
],
|
||||
"labware": "rep 2"
|
||||
},
|
||||
"dest_set_2": {
|
||||
"slot": 2,
|
||||
"well": [
|
||||
"A3",
|
||||
"A5",
|
||||
"A8"
|
||||
],
|
||||
"labware": "rep 2"
|
||||
},
|
||||
"Liquid_3": {
|
||||
"slot": 3,
|
||||
"well": [
|
||||
"A4",
|
||||
"A6",
|
||||
"A10"
|
||||
],
|
||||
"labware": "rep 3"
|
||||
},
|
||||
"Liquid_6": {
|
||||
"slot": 3,
|
||||
"well": [
|
||||
"A4",
|
||||
"A6",
|
||||
"A10"
|
||||
],
|
||||
"labware": "rep 3"
|
||||
},
|
||||
"dest_set_3": {
|
||||
"slot": 3,
|
||||
"well": [
|
||||
"A4",
|
||||
"A6",
|
||||
"A10"
|
||||
],
|
||||
"labware": "rep 3"
|
||||
},
|
||||
"cell_lines": {
|
||||
"slot": 4,
|
||||
"well": [
|
||||
"A1",
|
||||
"A3",
|
||||
"A5"
|
||||
],
|
||||
"labware": "DRUG + YOYO-MEDIA"
|
||||
},
|
||||
"cell_lines_2": {
|
||||
"slot": 4,
|
||||
"well": [
|
||||
"A1",
|
||||
"A3",
|
||||
"A5"
|
||||
],
|
||||
"labware": "DRUG + YOYO-MEDIA"
|
||||
},
|
||||
"cell_lines_3": {
|
||||
"slot": 4,
|
||||
"well": [
|
||||
"A1",
|
||||
"A3",
|
||||
"A5"
|
||||
],
|
||||
"labware": "DRUG + YOYO-MEDIA"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.10.15"
|
||||
__version__ = "0.11.2"
|
||||
|
||||
6
unilabos/__main__.py
Normal file
6
unilabos/__main__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""Entry point for `python -m unilabos`."""
|
||||
|
||||
from unilabos.app.main import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,30 +1,118 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
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))
|
||||
if unilabos_dir not in sys.path:
|
||||
sys.path.append(unilabos_dir)
|
||||
|
||||
from unilabos.app.utils import cleanup_for_restart
|
||||
from unilabos.utils.banner_print import print_status, print_unilab_banner
|
||||
from unilabos.config.config import load_config, BasicConfig, HTTPConfig
|
||||
from unilabos.app.utils import cleanup_for_restart
|
||||
|
||||
# Global restart flags (used by ws_client and web/server)
|
||||
_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:
|
||||
@@ -66,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,
|
||||
@@ -147,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(
|
||||
@@ -155,17 +250,53 @@ def parse_args():
|
||||
action="store_true",
|
||||
help="Skip environment dependency check on startup",
|
||||
)
|
||||
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 registry information",
|
||||
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",
|
||||
@@ -199,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
|
||||
|
||||
|
||||
@@ -210,69 +347,110 @@ 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
|
||||
|
||||
# 环境检查 - 检查并自动安装必需的包 (可选)
|
||||
if not args_dict.get("skip_env_check", False):
|
||||
from unilabos.utils.environment_check import check_environment
|
||||
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, 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 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",
|
||||
)
|
||||
os._exit(1)
|
||||
# === 解析 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(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 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")
|
||||
working_dir = os.path.dirname(os.path.abspath(config_path))
|
||||
else:
|
||||
working_dir = os.path.abspath(os.getcwd())
|
||||
|
||||
# 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")
|
||||
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)
|
||||
|
||||
# 加载配置文件 (check_mode 跳过)
|
||||
print_status(f"当前工作目录为 {working_dir}", "info")
|
||||
load_config_from_file(config_path)
|
||||
if not check_mode:
|
||||
load_config_from_file(config_path)
|
||||
|
||||
# 根据配置重新设置日志级别
|
||||
from unilabos.utils.log import configure_logger, logger
|
||||
|
||||
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"
|
||||
@@ -308,41 +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)
|
||||
|
||||
# 注册表
|
||||
# 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
|
||||
external_only = args_dict.get("external_devices_only", False)
|
||||
lab_registry = build_registry(
|
||||
args_dict["registry_path"], args_dict.get("complete_registry", False), BasicConfig.upload_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: 注册表验证完成后直接退出
|
||||
if check_mode:
|
||||
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:
|
||||
@@ -359,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
|
||||
@@ -427,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
|
||||
@@ -524,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,10 +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")
|
||||
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] 设备和资源注册完成.")
|
||||
|
||||
@@ -4,8 +4,181 @@ UniLabOS 应用工具函数
|
||||
提供清理、重启等工具函数
|
||||
"""
|
||||
|
||||
import gc
|
||||
import glob
|
||||
import os
|
||||
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 / 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 # noqa: F401
|
||||
|
||||
return
|
||||
except ImportError as e:
|
||||
if not str(e).startswith("DLL load failed"):
|
||||
return
|
||||
|
||||
cp = os.environ["CONDA_PREFIX"]
|
||||
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
|
||||
|
||||
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()
|
||||
|
||||
import gc
|
||||
import threading
|
||||
import time
|
||||
|
||||
|
||||
@@ -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,13 +398,12 @@ class HTTPClient:
|
||||
edges: 工作流边列表
|
||||
tags: 工作流标签列表,默认为空列表
|
||||
published: 是否发布工作流,默认为False
|
||||
description: 工作流描述,发布时使用
|
||||
|
||||
Returns:
|
||||
Dict: API响应数据,包含 code 和 data (uuid, name)
|
||||
"""
|
||||
# target_lab_uuid 暂时使用默认值,后续由后端根据 ak/sk 获取
|
||||
payload = {
|
||||
"target_lab_uuid": "28c38bb0-63f6-4352-b0d8-b5b8eb1766d5",
|
||||
"name": name,
|
||||
"data": {
|
||||
"workflow_uuid": workflow_uuid,
|
||||
@@ -369,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}"},
|
||||
@@ -390,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()
|
||||
|
||||
@@ -58,14 +58,14 @@ class JobResultStore:
|
||||
feedback=feedback or {},
|
||||
timestamp=time.time(),
|
||||
)
|
||||
logger.debug(f"[JobResultStore] Stored result for job {job_id[:8]}, status={status}")
|
||||
logger.trace(f"[JobResultStore] Stored result for job {job_id[:8]}, status={status}")
|
||||
|
||||
def get_and_remove(self, job_id: str) -> Optional[JobResult]:
|
||||
"""获取并删除任务结果"""
|
||||
with self._results_lock:
|
||||
result = self._results.pop(job_id, None)
|
||||
if result:
|
||||
logger.debug(f"[JobResultStore] Retrieved and removed result for job {job_id[:8]}")
|
||||
logger.trace(f"[JobResultStore] Retrieved and removed result for job {job_id[:8]}")
|
||||
return result
|
||||
|
||||
def get_result(self, job_id: str) -> Optional[JobResult]:
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -327,6 +328,7 @@ def job_add(req: JobAddReq) -> JobData:
|
||||
queue_item,
|
||||
action_type=action_type,
|
||||
action_kwargs=action_args,
|
||||
sample_material=req.sample_material,
|
||||
server_info=server_info,
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -23,9 +23,10 @@ from typing import Optional, Dict, Any, List
|
||||
from urllib.parse import urlparse
|
||||
from enum import Enum
|
||||
|
||||
from jedi.inference.gradual.typing import TypedDict
|
||||
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:
|
||||
# 有正在执行或准备执行的任务,加入队列
|
||||
@@ -154,7 +167,7 @@ class DeviceActionManager:
|
||||
job_info.set_ready_timeout(10) # 设置10秒超时
|
||||
self.active_jobs[device_key] = job_info
|
||||
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
||||
logger.info(f"[DeviceActionManager] Job {job_log} can start immediately for {device_key}")
|
||||
logger.trace(f"[DeviceActionManager] Job {job_log} can start immediately for {device_key}")
|
||||
return True
|
||||
|
||||
def start_job(self, job_id: str) -> bool:
|
||||
@@ -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]
|
||||
@@ -210,8 +234,9 @@ class DeviceActionManager:
|
||||
job_info.update_timestamp()
|
||||
# 从all_jobs中移除已结束的job
|
||||
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.info(f"[DeviceActionManager] Job {job_log} ended for {device_key}")
|
||||
# job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
||||
# logger.debug(f"[DeviceActionManager] Job {job_log} ended for {device_key}")
|
||||
pass
|
||||
else:
|
||||
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
||||
logger.warning(f"[DeviceActionManager] Job {job_log} was not active for {device_key}")
|
||||
@@ -227,15 +252,20 @@ class DeviceActionManager:
|
||||
next_job_log = format_job_log(
|
||||
next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name
|
||||
)
|
||||
logger.info(f"[DeviceActionManager] Next job {next_job_log} can start for {device_key}")
|
||||
logger.trace(f"[DeviceActionManager] Next job {next_job_log} can start for {device_key}")
|
||||
return next_job
|
||||
|
||||
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]:
|
||||
"""获取所有排队中的任务"""
|
||||
@@ -260,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状态
|
||||
@@ -268,7 +306,7 @@ class DeviceActionManager:
|
||||
# 从all_jobs中移除
|
||||
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.info(f"[DeviceActionManager] Active job {job_log} cancelled for {device_key}")
|
||||
logger.trace(f"[DeviceActionManager] Active job {job_log} cancelled for {device_key}")
|
||||
|
||||
# 启动下一个任务
|
||||
if device_key in self.device_queues and self.device_queues[device_key]:
|
||||
@@ -281,7 +319,7 @@ class DeviceActionManager:
|
||||
next_job_log = format_job_log(
|
||||
next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name
|
||||
)
|
||||
logger.info(f"[DeviceActionManager] Next job {next_job_log} can start after cancel")
|
||||
logger.trace(f"[DeviceActionManager] Next job {next_job_log} can start after cancel")
|
||||
return True
|
||||
|
||||
# 如果是排队中的任务
|
||||
@@ -295,7 +333,7 @@ class DeviceActionManager:
|
||||
job_log = format_job_log(
|
||||
job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name
|
||||
)
|
||||
logger.info(f"[DeviceActionManager] Queued job {job_log} cancelled for {device_key}")
|
||||
logger.trace(f"[DeviceActionManager] Queued job {job_log} cancelled for {device_key}")
|
||||
return True
|
||||
|
||||
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
||||
@@ -333,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(
|
||||
@@ -368,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}")
|
||||
@@ -394,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连接和重连逻辑"""
|
||||
@@ -426,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}",
|
||||
@@ -438,77 +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):
|
||||
"""处理发送队列中的消息"""
|
||||
@@ -540,7 +615,7 @@ class MessageProcessor:
|
||||
try:
|
||||
message_str = json.dumps(msg, ensure_ascii=False)
|
||||
await self.websocket.send(message_str)
|
||||
logger.trace(f"[MessageProcessor] Message sent: {msg.get('action', 'unknown')}") # type: ignore # noqa: E501
|
||||
# logger.trace(f"[MessageProcessor] Message sent: {msg.get('action', 'unknown')}") # type: ignore # noqa: E501
|
||||
except Exception as e:
|
||||
logger.error(f"[MessageProcessor] Failed to send message: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
@@ -557,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())
|
||||
@@ -565,7 +641,7 @@ class MessageProcessor:
|
||||
|
||||
async def _process_message(self, message_type: str, message_data: Dict[str, Any]):
|
||||
"""处理收到的消息"""
|
||||
logger.debug(f"[MessageProcessor] Processing message: {message_type}")
|
||||
logger.trace(f"[MessageProcessor] Processing message: {message_type}")
|
||||
|
||||
try:
|
||||
if message_type == "pong":
|
||||
@@ -588,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:
|
||||
@@ -603,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", "")
|
||||
@@ -610,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")
|
||||
@@ -617,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,
|
||||
)
|
||||
|
||||
# 添加到设备管理器
|
||||
@@ -635,15 +739,29 @@ 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.info(f"[MessageProcessor] Job {job_log} can start immediately")
|
||||
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.info(f"[MessageProcessor] Job {job_log} queued")
|
||||
logger.trace(f"[MessageProcessor] Job {job_log} queued")
|
||||
|
||||
# 通知QueueProcessor有新的队列更新
|
||||
if self.queue_processor:
|
||||
@@ -652,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}")
|
||||
@@ -670,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,
|
||||
)
|
||||
|
||||
@@ -683,6 +836,7 @@ class MessageProcessor:
|
||||
queue_item,
|
||||
action_type=req.action_type,
|
||||
action_kwargs=req.action_args,
|
||||
sample_material=req.sample_material,
|
||||
server_info=req.server_info,
|
||||
)
|
||||
|
||||
@@ -708,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": {},
|
||||
@@ -729,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
|
||||
@@ -847,9 +1003,7 @@ class MessageProcessor:
|
||||
device_action_groups[key_add] = []
|
||||
device_action_groups[key_add].append(item["uuid"])
|
||||
|
||||
logger.info(
|
||||
f"[资源同步] 跨站Transfer: {item['uuid'][:8]} from {device_old_id} to {device_id}"
|
||||
)
|
||||
logger.info(f"[资源同步] 跨站Transfer: {item['uuid'][:8]} from {device_old_id} to {device_id}")
|
||||
else:
|
||||
# 正常update
|
||||
key = (device_id, "update")
|
||||
@@ -863,7 +1017,9 @@ class MessageProcessor:
|
||||
device_action_groups[key] = []
|
||||
device_action_groups[key].append(item["uuid"])
|
||||
|
||||
logger.trace(f"[资源同步] 动作 {action} 分组数量: {len(device_action_groups)}, 总数量: {len(resource_uuid_list)}")
|
||||
logger.trace(
|
||||
f"[资源同步] 动作 {action} 分组数量: {len(device_action_groups)}, 总数量: {len(resource_uuid_list)}"
|
||||
)
|
||||
|
||||
# 为每个(device_id, action)创建独立的更新线程
|
||||
for (device_id, actual_action), items in device_action_groups.items():
|
||||
@@ -899,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]):
|
||||
"""
|
||||
处理重启请求
|
||||
@@ -910,14 +1097,13 @@ 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
|
||||
|
||||
main_module._restart_requested = True
|
||||
main_module._restart_reason = reason
|
||||
|
||||
@@ -927,10 +1113,12 @@ class MessageProcessor:
|
||||
# 在新线程中执行清理,避免阻塞当前事件循环
|
||||
def do_cleanup():
|
||||
import time
|
||||
|
||||
time.sleep(0.5) # 给当前消息处理完成的时间
|
||||
logger.info(f"[MessageProcessor] Starting cleanup for restart, reason: {reason}")
|
||||
try:
|
||||
from unilabos.app.utils import cleanup_for_restart
|
||||
|
||||
if cleanup_for_restart():
|
||||
logger.info("[MessageProcessor] Cleanup successful, main() will restart")
|
||||
else:
|
||||
@@ -943,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 = {
|
||||
@@ -954,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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1013,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")
|
||||
@@ -1035,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完成流程
|
||||
@@ -1093,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)
|
||||
@@ -1110,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": {
|
||||
@@ -1121,14 +1332,15 @@ 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)
|
||||
job_log = format_job_log(job_info.job_id, job_info.task_id, job_info.device_id, job_info.action_name)
|
||||
if success:
|
||||
logger.debug(f"[QueueProcessor] Sent busy/need_more for queued job {job_log}")
|
||||
logger.trace(f"[QueueProcessor] Sent busy/need_more for queued job {job_log}")
|
||||
else:
|
||||
logger.warning(f"[QueueProcessor] Failed to send busy status for job {job_log}")
|
||||
|
||||
@@ -1151,7 +1363,7 @@ class QueueProcessor:
|
||||
job_info.action_name,
|
||||
)
|
||||
|
||||
logger.info(f"[QueueProcessor] Job {job_log} completed with status: {status}")
|
||||
logger.trace(f"[QueueProcessor] Job {job_log} completed with status: {status}")
|
||||
|
||||
# 结束任务,获取下一个可执行的任务
|
||||
next_job = self.device_manager.end_job(job_id)
|
||||
@@ -1166,13 +1378,16 @@ 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)
|
||||
logger.info(f"[QueueProcessor] Notified next job {next_job_log} can start")
|
||||
# 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")
|
||||
|
||||
# 立即触发下一轮状态检查
|
||||
self.notify_queue_update()
|
||||
@@ -1205,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)
|
||||
@@ -1261,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)}")
|
||||
|
||||
@@ -1294,7 +1513,7 @@ class WebSocketClient(BaseCommunicationClient):
|
||||
},
|
||||
}
|
||||
self.message_processor.send_message(message)
|
||||
logger.trace(f"[WebSocketClient] Device status published: {device_id}.{property_name}")
|
||||
# logger.trace(f"[WebSocketClient] Device status published: {device_id}.{property_name}")
|
||||
|
||||
def publish_job_status(
|
||||
self, feedback_data: dict, item: QueueItem, status: str, return_info: Optional[dict] = None
|
||||
@@ -1304,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.info(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,
|
||||
@@ -1335,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:
|
||||
@@ -1381,7 +1610,9 @@ class WebSocketClient(BaseCommunicationClient):
|
||||
if host_node:
|
||||
# 获取设备信息
|
||||
for device_id, namespace in host_node.devices_names.items():
|
||||
device_key = f"{namespace}/{device_id}" if namespace.startswith("/") else f"/{namespace}/{device_id}"
|
||||
device_key = (
|
||||
f"{namespace}/{device_id}" if namespace.startswith("/") else f"/{namespace}/{device_id}"
|
||||
)
|
||||
is_online = device_key in host_node._online_devices
|
||||
|
||||
# 获取设备的动作信息
|
||||
@@ -1395,14 +1626,16 @@ class WebSocketClient(BaseCommunicationClient):
|
||||
"action_type": str(type(client).__name__),
|
||||
}
|
||||
|
||||
devices.append({
|
||||
"device_id": device_id,
|
||||
"namespace": namespace,
|
||||
"device_key": device_key,
|
||||
"is_online": is_online,
|
||||
"machine_name": host_node.device_machine_names.get(device_id, machine_name),
|
||||
"actions": actions,
|
||||
})
|
||||
devices.append(
|
||||
{
|
||||
"device_id": device_id,
|
||||
"namespace": namespace,
|
||||
"device_key": device_key,
|
||||
"is_online": is_online,
|
||||
"machine_name": host_node.device_machine_names.get(device_id, machine_name),
|
||||
"actions": actions,
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(f"[WebSocketClient] Collected {len(devices)} devices for host_ready")
|
||||
except Exception as e:
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -22,6 +22,9 @@ class BasicConfig:
|
||||
startup_json_path = None # 填写绝对路径
|
||||
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"
|
||||
|
||||
@@ -38,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配置
|
||||
@@ -144,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)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user