Compare commits

..

13 Commits

Author SHA1 Message Date
h840473807
5805f94e9a 扣电驱动中增加多个组装参数,更新驱动与注册表 (#120)
扣电驱动中增加多个组装参数,elec_vol:int=50, assembly_type:int=7, assembly_pressure:int=4200,更新驱动与注册表
2025-10-21 16:27:02 +08:00
Calvin Cao
3adcc41ce8 Merge pull request #118 from h840473807/workstation_dev_YB2
Workstation dev yb2
2025-10-21 10:32:41 +08:00
h840473807
243922caf4 宜宾配液+扣电工站注册表文件
宜宾配液+扣电工站注册表文件
2025-10-20 15:43:21 +08:00
h840473807
079ec9d1b4 workstation_by_hhm
宜宾扣电工站与奔曜配液工站,更新截止10月20日
2025-10-20 15:36:53 +08:00
ZiWei
54cfaf15f3 Workstation dev yb2 (#100)
* Refactor and extend reaction station action messages

* Refactor dispensing station tasks to enhance parameter clarity and add batch processing capabilities

- Updated `create_90_10_vial_feeding_task` to include detailed parameters for 90%/10% vial feeding, improving clarity and usability.
- Introduced `create_batch_90_10_vial_feeding_task` for batch processing of 90%/10% vial feeding tasks with JSON formatted input.
- Added `create_batch_diamine_solution_task` for batch preparation of diamine solution, also utilizing JSON formatted input.
- Refined `create_diamine_solution_task` to include additional parameters for better task configuration.
- Enhanced schema descriptions and default values for improved user guidance.
2025-10-10 15:25:50 +08:00
Junhan Chang
1c9d2ee98a fix bioyond resource io 2025-09-30 17:02:38 +08:00
Junhan Chang
3fe8f4ca44 add child_size for itemized_carrier 2025-09-30 12:58:42 +08:00
Junhan Chang
2476821dcc update bioyond launch json 2025-09-30 12:25:21 +08:00
Junhan Chang
7b426ed5ae create warehouse by factory func 2025-09-30 11:57:34 +08:00
Junhan Chang
9bbae96447 Merge branch 'workstation_dev_YB2' of https://github.com/dptech-corp/Uni-Lab-OS into workstation_dev_YB2 2025-09-29 21:02:05 +08:00
Junhan Chang
10aabb7592 refactor: add itemized_carrier instead of carrier consists of ResourceHolder 2025-09-29 20:36:45 +08:00
Junhan Chang
a5397ffe12 create/update resources with POST/PUT for big amount/ small amount data 2025-09-26 23:25:34 +08:00
Junhan Chang
196e0f7e2b fix bioyond station and registry 2025-09-26 08:12:41 +08:00
60 changed files with 32801 additions and 1383 deletions

View File

@@ -127,16 +127,16 @@ add_action_files(
```bash
mamba remove --force ros-humble-unilabos-msgs
mamba config set safety_checks disabled # 如果没有提升版本号会触发md5与网络上md5不一致是正常现象因此通过本指令关闭md5检查
mamba install xxx.conda2 --offline
mamba install xxx.conda --offline
```
## 常见问题
**Q: 构建失败怎么办?**
**Q: 构建失败怎么办?**
A: 检查 Actions 日志中的错误信息,通常是语法错误或依赖问题。修复后重新推送代码即可自动触发新的构建。
**Q: 如何测试特定平台?**
**Q: 如何测试特定平台?**
A: 在手动触发构建时,在平台选择中只填写你需要的平台,如 `linux-64` 或 `win-64`。
**Q: 构建包在哪里下载?**
**Q: 构建包在哪里下载?**
A: 在 Actions 页面的构建结果中,查找 "Artifacts" 部分,每个平台都有对应的构建包可供下载。

View File

@@ -0,0 +1,60 @@
{
"nodes": [
{
"id": "dispensing_station_bioyond",
"name": "dispensing_station_bioyond",
"children": [
"Bioyond_Dispensing_Deck"
],
"parent": null,
"type": "device",
"class": "dispensing_station.bioyond",
"config": {
"config": {
"api_key": "DE9BDDA0",
"api_host": "http://192.168.1.200:44388"
},
"deck": {
"data": {
"_resource_child_name": "Bioyond_Dispensing_Deck",
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerPreparationStation_Deck"
}
},
"station_config": {
"station_type": "dispensing_station",
"enable_dispensing_station": true,
"enable_reaction_station": false,
"station_name": "DispensingStation_001",
"description": "Bioyond配液工作站"
},
"protocol_type": []
},
"data": {}
},
{
"id": "Bioyond_Dispensing_Deck",
"name": "Bioyond_Dispensing_Deck",
"sample_id": null,
"children": [],
"parent": "dispensing_station_bioyond",
"type": "deck",
"class": "BIOYOND_PolymerPreparationStation_Deck",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"type": "BIOYOND_PolymerPreparationStation_Deck",
"setup": true,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
}
},
"data": {}
}
]
}

View File

@@ -0,0 +1,69 @@
{
"nodes": [
{
"id": "reaction_station_bioyond",
"name": "reaction_station_bioyond",
"parent": null,
"children": [
"Bioyond_Deck"
],
"type": "device",
"class": "reaction_station.bioyond",
"config": {
"bioyond_config": {
"api_key": "DE9BDDA0",
"api_host": "http://192.168.1.200:44402",
"workflow_mappings": {
"reactor_taken_out": "3a16081e-4788-ca37-eff4-ceed8d7019d1",
"reactor_taken_in": "3a160df6-76b3-0957-9eb0-cb496d5721c6",
"Solid_feeding_vials": "3a160877-87e7-7699-7bc6-ec72b05eb5e6",
"Liquid_feeding_vials(non-titration)": "3a167d99-6158-c6f0-15b5-eb030f7d8e47",
"Liquid_feeding_solvents": "3a160824-0665-01ed-285a-51ef817a9046",
"Liquid_feeding(titration)": "3a160824-0665-01ed-285a-51ef817a9046",
"Liquid_feeding_beaker": "3a16087e-124f-8ddb-8ec1-c2dff09ca784",
"Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a"
},
"material_type_mappings": {
"烧杯": "BIOYOND_PolymerStation_1FlaskCarrier",
"试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier",
"样品板": "BIOYOND_PolymerStation_6VialCarrier"
}
},
"deck": {
"data": {
"_resource_child_name": "Bioyond_Deck",
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerReactionStation_Deck"
}
},
"protocol_type": []
},
"data": {}
},
{
"id": "Bioyond_Deck",
"name": "Bioyond_Deck",
"sample_id": null,
"children": [
],
"parent": "reaction_station_bioyond",
"type": "deck",
"class": "BIOYOND_PolymerReactionStation_Deck",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"type": "BIOYOND_PolymerReactionStation_Deck",
"setup": true,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
}
},
"data": {}
}
]
}

View File

@@ -0,0 +1,69 @@
{
"nodes": [
{
"id": "reaction_station_bioyond",
"name": "reaction_station_bioyond",
"parent": null,
"children": [
"Bioyond_Deck"
],
"type": "device",
"class": "workstation.bioyond",
"config": {
"bioyond_config": {
"api_key": "DE9BDDA0",
"api_host": "http://192.168.1.200:44388",
"workflow_mappings": {
"reactor_taken_out": "3a16081e-4788-ca37-eff4-ceed8d7019d1",
"reactor_taken_in": "3a160df6-76b3-0957-9eb0-cb496d5721c6",
"Solid_feeding_vials": "3a160877-87e7-7699-7bc6-ec72b05eb5e6",
"Liquid_feeding_vials(non-titration)": "3a167d99-6158-c6f0-15b5-eb030f7d8e47",
"Liquid_feeding_solvents": "3a160824-0665-01ed-285a-51ef817a9046",
"Liquid_feeding(titration)": "3a160824-0665-01ed-285a-51ef817a9046",
"Liquid_feeding_beaker": "3a16087e-124f-8ddb-8ec1-c2dff09ca784",
"Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a"
},
"material_type_mappings": {
"烧杯": "BIOYOND_PolymerStation_1FlaskCarrier",
"试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier",
"样品板": "BIOYOND_PolymerStation_6VialCarrier"
}
},
"deck": {
"data": {
"_resource_child_name": "Bioyond_Deck",
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerReactionStation_Deck"
}
},
"protocol_type": []
},
"data": {}
},
{
"id": "Bioyond_Deck",
"name": "Bioyond_Deck",
"sample_id": null,
"children": [
],
"parent": "reaction_station_bioyond",
"type": "deck",
"class": "BIOYOND_PolymerReactionStation_Deck",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"type": "BIOYOND_PolymerReactionStation_Deck",
"setup": true,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
}
},
"data": {}
}
]
}

View File

@@ -25,6 +25,7 @@ class HTTPClient:
remote_addr: 远程服务器地址,如果不提供则从配置中获取
auth: 授权信息
"""
self.initialized = False
self.remote_addr = remote_addr or HTTPConfig.remote_addr
if auth is not None:
self.auth = auth
@@ -69,12 +70,22 @@ class HTTPClient:
Returns:
Response: API响应对象
"""
response = requests.post(
f"{self.remote_addr}/lab/material",
json={"nodes": resources},
headers={"Authorization": f"Lab {self.auth}"},
timeout=100,
)
if not self.initialized:
self.initialized = True
info(f"首次添加资源,当前远程地址: {self.remote_addr}")
response = requests.post(
f"{self.remote_addr}/lab/material",
json={"nodes": resources},
headers={"Authorization": f"Lab {self.auth}"},
timeout=100,
)
else:
response = requests.put(
f"{self.remote_addr}/lab/material",
json={"nodes": resources},
headers={"Authorization": f"Lab {self.auth}"},
timeout=100,
)
if response.status_code == 200:
res = response.json()
if "code" in res and res["code"] != 0:
@@ -130,12 +141,22 @@ class HTTPClient:
Returns:
Response: API响应对象
"""
response = requests.put(
f"{self.remote_addr}/lab/material",
json={"nodes": resources},
headers={"Authorization": f"Lab {self.auth}"},
timeout=100,
)
if not self.initialized:
self.initialized = True
info(f"首次添加资源,当前远程地址: {self.remote_addr}")
response = requests.post(
f"{self.remote_addr}/lab/material",
json={"nodes": resources},
headers={"Authorization": f"Lab {self.auth}"},
timeout=100,
)
else:
response = requests.put(
f"{self.remote_addr}/lab/material",
json={"nodes": resources},
headers={"Authorization": f"Lab {self.auth}"},
timeout=100,
)
if response.status_code == 200:
res = response.json()
if "code" in res and res["code"] != 0:

View File

@@ -0,0 +1,49 @@
import requests
import json
from datetime import datetime
def test_benyao_api():
# 配置信息
ip_addr = "192.168.1.200"
port = 44386
#url = f"http://{ip_addr}:{port}/api/lims/scheduler/scheduler-status"
#url = f"http://{ip_addr}:{port}/api/lims/order/order-list-status"
url = f"http://{ip_addr}:{port}/api/lims/storage/stock-material"
apiKey = "8A819E5C" # 请替换为实际apiKey
# 构造请求体
request_data = {
"apiKey": apiKey,
"requestTime": datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ"), # 示例2025-08-15T10:00:00.000Z
"data": {
"typeMode": 1,
"includeDetail": True
}
}
#request_data = {
# "apiKey": apiKey,
# "requestTime": datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ"), # 示例2025-08-15T10:00:00.000Z
# "data":
#}
print(request_data)
# 发送POST请求
try:
response = requests.post(url, json=request_data, timeout=10)
response.raise_for_status() # 检查HTTP状态码
# 解析响应
result = response.json()
print("响应状态码:", response.status_code)
print("响应内容:")
print(json.dumps(result, indent=2, ensure_ascii=False))
except requests.exceptions.RequestException as e:
print("请求失败:", e)
except json.JSONDecodeError as e:
print("JSON解析失败:", e)
if __name__ == "__main__":
test_benyao_api()

View File

@@ -0,0 +1,374 @@
"""
Bioyond物料管理实现
Bioyond Material Management Implementation
基于Bioyond系统的物料管理支持从Bioyond系统同步物料到UniLab工作站
"""
from typing import Dict, Any, List, Optional, Union
import json
import asyncio
from abc import ABC, abstractmethod
from pylabrobot.resources import (
Resource as PLRResource,
Container,
Deck,
Coordinate as PLRCoordinate,
)
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
from unilabos.utils.log import logger
from unilabos.resources.graphio import (
resource_plr_to_ulab,
resource_ulab_to_plr,
resource_bioyond_to_ulab,
resource_bioyond_container_to_ulab,
resource_ulab_to_bioyond
)
from .workstation_material_management import MaterialManagementBase
class BioyondMaterialManagement(MaterialManagementBase):
"""Bioyond物料管理类
实现从Bioyond系统同步物料到UniLab工作站的功能
1. 从Bioyond系统获取物料数据
2. 转换为UniLab格式
3. 同步到PyLabRobot Deck
4. 支持双向同步
"""
def __init__(
self,
device_id: str,
deck_config: Dict[str, Any],
resource_tracker: DeviceNodeResourceTracker,
children_config: Dict[str, Dict[str, Any]] = None,
bioyond_config: Dict[str, Any] = None
):
self.bioyond_config = bioyond_config or {}
self.bioyond_api_client = None
self.sync_interval = self.bioyond_config.get("sync_interval", 30) # 同步间隔(秒)
# 初始化父类
super().__init__(device_id, deck_config, resource_tracker, children_config)
# 初始化Bioyond API客户端
self._initialize_bioyond_client()
# 启动同步任务
self._start_sync_task()
def _initialize_bioyond_client(self):
"""初始化Bioyond API客户端"""
try:
# 这里应该根据实际的Bioyond API实现
# 暂时使用模拟客户端
self.bioyond_api_client = BioyondAPIClient(self.bioyond_config)
logger.info(f"Bioyond API客户端初始化成功")
except Exception as e:
logger.error(f"Bioyond API客户端初始化失败: {e}")
self.bioyond_api_client = None
def _start_sync_task(self):
"""启动同步任务"""
if self.bioyond_api_client:
# 创建异步同步任务
asyncio.create_task(self._periodic_sync())
logger.info(f"Bioyond同步任务已启动间隔: {self.sync_interval}")
async def _periodic_sync(self):
"""定期同步任务"""
while True:
try:
await self.sync_from_bioyond()
await asyncio.sleep(self.sync_interval)
except Exception as e:
logger.error(f"Bioyond同步任务出错: {e}")
await asyncio.sleep(self.sync_interval)
async def sync_from_bioyond(self) -> bool:
"""从Bioyond系统同步物料"""
try:
if not self.bioyond_api_client:
logger.warning("Bioyond API客户端未初始化")
return False
# 1. 从Bioyond获取物料数据
bioyond_data = await self.bioyond_api_client.get_materials()
if not bioyond_data:
logger.warning("从Bioyond获取物料数据为空")
return False
# 2. 转换为UniLab格式
if isinstance(bioyond_data, dict) and "data" in bioyond_data:
# 容器格式数据
unilab_resources = resource_bioyond_container_to_ulab(bioyond_data)
else:
# 物料列表格式数据
unilab_resources = resource_bioyond_to_ulab(bioyond_data)
# 3. 转换为PLR格式并分配到Deck
await self._assign_resources_to_deck(unilab_resources)
logger.info(f"从Bioyond同步了 {len(unilab_resources)} 个资源")
return True
except Exception as e:
logger.error(f"从Bioyond同步物料失败: {e}")
return False
async def sync_to_bioyond(self, plr_resource: PLRResource) -> bool:
"""将本地物料变更同步到Bioyond系统"""
try:
if not self.bioyond_api_client:
logger.warning("Bioyond API客户端未初始化")
return False
# 1. 转换为UniLab格式
unilab_resource = resource_plr_to_ulab(plr_resource)
# 2. 转换为Bioyond格式
bioyond_materials = resource_ulab_to_bioyond([unilab_resource])
# 3. 发送到Bioyond系统
success = await self.bioyond_api_client.update_materials(bioyond_materials)
if success:
logger.info(f"成功同步物料 {plr_resource.name} 到Bioyond")
else:
logger.warning(f"同步物料 {plr_resource.name} 到Bioyond失败")
return success
except Exception as e:
logger.error(f"同步物料到Bioyond失败: {e}")
return False
async def _assign_resources_to_deck(self, unilab_resources: List[Dict[str, Any]]):
"""将UniLab资源分配到Deck"""
try:
# 转换为PLR格式
from unilabos.resources.graphio import list_to_nested_dict
nested_resources = list_to_nested_dict(unilab_resources)
plr_resources = resource_ulab_to_plr(nested_resources)
# 分配资源到Deck
if hasattr(plr_resources, 'children'):
resources_to_assign = plr_resources.children
elif isinstance(plr_resources, list):
resources_to_assign = plr_resources
else:
resources_to_assign = [plr_resources]
for resource in resources_to_assign:
try:
# 获取资源位置
if hasattr(resource, 'location') and resource.location:
location = PLRCoordinate(resource.location.x, resource.location.y, resource.location.z)
else:
location = PLRCoordinate(0, 0, 0)
# 分配资源到Deck
self.plr_deck.assign_child_resource(resource, location)
# 注册到resource tracker
self.resource_tracker.add_resource(resource)
# 保存资源引用
self.plr_resources[resource.name] = resource
except Exception as e:
logger.error(f"分配资源 {resource.name} 到Deck失败: {e}")
logger.info(f"成功分配了 {len(resources_to_assign)} 个资源到Deck")
except Exception as e:
logger.error(f"分配资源到Deck失败: {e}")
def _create_resource_by_type(
self,
resource_id: str,
resource_type: str,
config: Dict[str, Any],
data: Dict[str, Any],
location: PLRCoordinate
) -> Optional[PLRResource]:
"""根据类型创建Bioyond相关资源"""
try:
# 这里可以根据需要实现特定的Bioyond资源类型
# 目前使用通用的容器类型
if resource_type in ["container", "plate", "well"]:
return self._create_generic_container(resource_id, resource_type, config, data, location)
else:
logger.warning(f"未知的Bioyond资源类型: {resource_type}")
return None
except Exception as e:
logger.error(f"创建Bioyond资源失败 {resource_id} ({resource_type}): {e}")
return None
def _create_generic_container(
self,
resource_id: str,
resource_type: str,
config: Dict[str, Any],
data: Dict[str, Any],
location: PLRCoordinate
) -> Optional[PLRResource]:
"""创建通用容器资源"""
try:
from pylabrobot.resources import Plate, Well
if resource_type == "plate":
return Plate(
name=resource_id,
size_x=config.get("size_x", 127.76),
size_y=config.get("size_y", 85.48),
size_z=config.get("size_z", 14.35),
location=location,
category="plate"
)
elif resource_type == "well":
return Well(
name=resource_id,
size_x=config.get("size_x", 9.0),
size_y=config.get("size_y", 9.0),
size_z=config.get("size_z", 10.0),
location=location,
category="well"
)
else:
return Container(
name=resource_id,
size_x=config.get("size_x", 50.0),
size_y=config.get("size_y", 50.0),
size_z=config.get("size_z", 10.0),
location=location,
category="container"
)
except Exception as e:
logger.error(f"创建通用容器失败 {resource_id}: {e}")
return None
def get_bioyond_materials(self) -> List[Dict[str, Any]]:
"""获取当前Bioyond物料列表"""
try:
# 将当前PLR资源转换为Bioyond格式
bioyond_materials = []
for resource in self.plr_resources.values():
unilab_resource = resource_plr_to_ulab(resource)
bioyond_materials.extend(resource_ulab_to_bioyond([unilab_resource]))
return bioyond_materials
except Exception as e:
logger.error(f"获取Bioyond物料列表失败: {e}")
return []
def update_material_from_bioyond(self, material_id: str, bioyond_data: Dict[str, Any]) -> bool:
"""从Bioyond数据更新指定物料"""
try:
# 查找现有物料
material = self.find_material_by_id(material_id)
if not material:
logger.warning(f"未找到物料: {material_id}")
return False
# 转换Bioyond数据为UniLab格式
unilab_resources = resource_bioyond_to_ulab([bioyond_data])
if not unilab_resources:
logger.warning(f"转换Bioyond数据失败: {material_id}")
return False
# 更新物料属性
unilab_resource = unilab_resources[0]
material.name = unilab_resource.get("name", material.name)
# 更新位置
position = unilab_resource.get("position", {})
if position:
material.location = PLRCoordinate(
position.get("x", 0),
position.get("y", 0),
position.get("z", 0)
)
logger.info(f"成功更新物料: {material_id}")
return True
except Exception as e:
logger.error(f"更新物料失败 {material_id}: {e}")
return False
class BioyondAPIClient:
"""Bioyond API客户端模拟实现
实际使用时需要根据Bioyond系统的API接口实现
"""
def __init__(self, config: Dict[str, Any]):
self.config = config
self.base_url = config.get("base_url", "http://localhost:8080")
self.api_key = config.get("api_key", "")
self.timeout = config.get("timeout", 30)
async def get_materials(self) -> Optional[Union[Dict[str, Any], List[Dict[str, Any]]]]:
"""从Bioyond系统获取物料数据"""
try:
# 这里应该实现实际的API调用
# 暂时返回模拟数据
logger.info("从Bioyond API获取物料数据")
# 模拟API调用延迟
await asyncio.sleep(0.1)
# 返回模拟数据实际应该从API获取
return {
"data": [],
"code": 1,
"message": "success",
"timestamp": 1234567890
}
except Exception as e:
logger.error(f"Bioyond API调用失败: {e}")
return None
async def update_materials(self, materials: List[Dict[str, Any]]) -> bool:
"""更新Bioyond系统中的物料数据"""
try:
# 这里应该实现实际的API调用
logger.info(f"更新Bioyond系统中的 {len(materials)} 个物料")
# 模拟API调用延迟
await asyncio.sleep(0.1)
# 模拟成功响应
return True
except Exception as e:
logger.error(f"更新Bioyond物料失败: {e}")
return False
async def get_material_by_id(self, material_id: str) -> Optional[Dict[str, Any]]:
"""根据ID获取单个物料"""
try:
# 这里应该实现实际的API调用
logger.info(f"从Bioyond API获取物料: {material_id}")
# 模拟API调用延迟
await asyncio.sleep(0.1)
# 返回模拟数据
return {
"id": material_id,
"name": f"material_{material_id}",
"type": "container",
"quantity": 1.0,
"unit": ""
}
except Exception as e:
logger.error(f"获取Bioyond物料失败 {material_id}: {e}")
return None

View File

@@ -0,0 +1,796 @@
# -*- coding: utf-8 -*-
from typing import Dict, Any, List, Optional
from datetime import datetime, timezone
import requests
from pathlib import Path
import pandas as pd
import time
from datetime import datetime, timezone, timedelta
import re
import threading
from urllib3 import response
from unilabos.devices.workstation.workstation_base import WorkstationBase
from unilabos.devices.workstation.workstation_http_service import WorkstationHTTPService
from unilabos.utils.log import logger
from pylabrobot.resources.deck import Deck
def _iso_utc_now_ms() -> str:
# 文档要求:到毫秒 + Z例如 2025-08-15T05:43:22.814Z
dt = datetime.now()
return dt.strftime("%Y-%m-%dT%H:%M:%S.") + f"{int(dt.microsecond/1000):03d}Z"
class BioyondWorkstation(WorkstationBase):
"""
集成 Bioyond LIMS 的工作站示例,
覆盖:入库(2.17/2.18) → 新建实验(2.14) → 启动调度(2.7) →
运行中推送:物料变更(2.24)、步骤完成(2.21)、订单完成(2.23) →
查询实验(2.5/2.6) → 3-2-1 转运(2.32) → 样品/废料取出(2.28)
"""
def __init__(
self,
bioyond_config: Optional[Dict[str, Any]] = None,
station_resource: Optional[Dict[str, Any]] = None,
debug_mode: bool = False,
*args, **kwargs,
):
default_config = {
#"base_url": "http://192.168.1.200:44388",
"base_url": "http://61.169.57.196:44422",
"api_key": "8A819E5C",
"timeout": 30,
"report_token": "CHANGE_ME_TOKEN"
}
self.bioyond_config = {**default_config, **(bioyond_config or {})}
self.http_service_started = False
self.debug_mode = debug_mode
super().__init__(deck=Deck, station_resource=station_resource, *args, **kwargs)
logger.info(f"Bioyond工作站初始化完成 (debug_mode={self.debug_mode})")
# 实例化并在后台线程启动 HTTP 报送服务
# self.order_status = {}
# try:
# t = threading.Thread(target=self._start_http_service_bg, daemon=True, name="unilab_http")
# t.start()
# except Exception as e:
# logger.error(f"unilab-server后台启动报送服务失败: {e}")
# @property
# def device_id(self) -> str:
# try:
# return getattr(self, "_ros_node").device_id # 兼容 ROS 场景
# except Exception:
# return "bioyond_workstation"
# def _start_http_service_bg(self, host: str = "192.168.1.104", port: int = 8080) -> None:
# logger.info("进入 _start_http_service_bg 函数")
# try:
# self.service = WorkstationHTTPService(self, host=host, port=port)
# logger.info("WorkstationHTTPService 实例化完成")
# self.service.start()
# self.http_service_started = True
# logger.info(f"unilab_HTTP 服务成功启动: {host}:{port}")
# 一直挂着,直到进程退出
# while True:
# time.sleep(1)
# except Exception as e:
# self.http_service_started = False
# logger.error(f"启动unilab_HTTP服务失败: {e}", exc_info=True)
# -------------------- 基础HTTP封装 --------------------
def _url(self, path: str) -> str:
return f"{self.bioyond_config['base_url'].rstrip('/')}/{path.lstrip('/')}"
def _post_lims(self, path: str, data: Optional[Any] = None) -> Dict[str, Any]:
"""LIMS API大多数接口用 {apiKey/requestTime,data} 包装"""
payload = {
"apiKey": self.bioyond_config["api_key"],
"requestTime": _iso_utc_now_ms()
}
if data is not None:
payload["data"] = data
if self.debug_mode:
# 模拟返回,不发真实请求
logger.info(f"[DEBUG] POST {path} with payload={payload}")
return {"debug": True, "url": self._url(path), "payload": payload, "status": "ok"}
try:
r = requests.post(
self._url(path),
json=payload,
timeout=self.bioyond_config.get("timeout", 30),
headers={"Content-Type": "application/json"}
)
r.raise_for_status()
#print(r.json())
return r.json()
except Exception as e:
logger.error(f"POST {path} 失败: {e}")
return {"error": str(e)}
# --- 修正_post_report / _post_report_raw 同样走 debug_mode ---
def _post_report(self, path: str, data: Dict[str, Any]) -> Dict[str, Any]:
payload = {
"token": self.bioyond_config.get("report_token", ""),
"request_time": _iso_utc_now_ms(),
"data": data
}
if self.debug_mode:
logger.info(f"[DEBUG] POST {path} with payload={payload}")
return {"debug": True, "url": self._url(path), "payload": payload, "status": "ok"}
try:
r = requests.post(self._url(path), json=payload,
timeout=self.bioyond_config.get("timeout", 30),
headers={"Content-Type": "application/json"})
r.raise_for_status()
return r.json()
except Exception as e:
logger.error(f"POST {path} 失败: {e}")
return {"error": str(e)}
def _post_report_raw(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]:
if self.debug_mode:
logger.info(f"[DEBUG] POST {path} with body={body}")
return {"debug": True, "url": self._url(path), "payload": body, "status": "ok"}
try:
r = requests.post(self._url(path), json=body,
timeout=self.bioyond_config.get("timeout", 30),
headers={"Content-Type": "application/json"})
r.raise_for_status()
return r.json()
except Exception as e:
logger.error(f"POST {path} 失败: {e}")
return {"error": str(e)}
# -------------------- 单点接口封装 --------------------
# 2.17 入库物料(单个)
def storage_inbound(self, material_id: str, location_id: str) -> Dict[str, Any]:
return self._post_lims("/api/lims/storage/inbound", {
"materialId": material_id,
"locationId": location_id
})
# 2.18 批量入库(多个)
def storage_batch_inbound(self, items: List[Dict[str, str]]) -> Dict[str, Any]:
"""
items = [{"materialId": "...", "locationId": "..."}, ...]
"""
return self._post_lims("/api/lims/storage/batch-inbound", items)
# 3.30 自动化上料Excel -> JSON -> POST /api/lims/order/auto-feeding4to3
def auto_feeding4to3_from_xlsx(self, xlsx_path: str) -> Dict[str, Any]:
"""
根据固定模板解析 Excel
- 四号手套箱加样头面 (2-13行, 3-7列)
- 四号手套箱原液瓶面 (15-23行, 3-9列)
- 三号手套箱人工堆栈 (26-40行, 3-7列)
"""
path = Path(xlsx_path)
if not path.exists():
raise FileNotFoundError(f"未找到 Excel 文件:{path}")
try:
df = pd.read_excel(path, sheet_name=0, header=None, engine="openpyxl")
except Exception as e:
raise RuntimeError(f"读取 Excel 失败:{e}")
items: List[Dict[str, Any]] = []
# 四号手套箱 - 加样头面2-13行, 3-7列
for _, row in df.iloc[1:13, 2:7].iterrows():
item = {
"sourceWHName": "四号手套箱堆栈",
"posX": int(row[2]),
"posY": int(row[3]),
"posZ": int(row[4]),
"materialName": str(row[5]).strip() if pd.notna(row[5]) else "",
"quantity": float(row[6]) if pd.notna(row[6]) else 0.0,
}
if item["materialName"]:
items.append(item)
# 四号手套箱 - 原液瓶面15-23行, 3-9列
for _, row in df.iloc[14:23, 2:9].iterrows():
item = {
"sourceWHName": "四号手套箱堆栈",
"posX": int(row[2]),
"posY": int(row[3]),
"posZ": int(row[4]),
"materialName": str(row[5]).strip() if pd.notna(row[5]) else "",
"quantity": float(row[6]) if pd.notna(row[6]) else 0.0,
"materialType": str(row[7]).strip() if pd.notna(row[7]) else "",
"targetWH": str(row[8]).strip() if pd.notna(row[8]) else "",
}
if item["materialName"]:
items.append(item)
# 三号手套箱人工堆栈26-40行, 3-7列
for _, row in df.iloc[25:40, 2:7].iterrows():
item = {
"sourceWHName": "三号手套箱人工堆栈",
"posX": int(row[2]),
"posY": int(row[3]),
"posZ": int(row[4]),
"materialType": str(row[5]).strip() if pd.notna(row[5]) else "",
"materialId": str(row[6]).strip() if pd.notna(row[6]) else "",
"quantity": 1 # 默认数量1
}
if item["materialId"] or item["materialType"]:
items.append(item)
return self._post_lims("/api/lims/order/auto-feeding4to3", items)
def auto_batch_outbound_from_xlsx(self, xlsx_path: str) -> Dict[str, Any]:
"""
3.31 自动化下料Excel -> JSON -> POST /api/lims/storage/auto-batch-out-bound
"""
path = Path(xlsx_path)
if not path.exists():
raise FileNotFoundError(f"未找到 Excel 文件:{path}")
try:
df = pd.read_excel(path, sheet_name=0, engine="openpyxl")
except Exception as e:
raise RuntimeError(f"读取 Excel 失败:{e}")
def pick(names: List[str]) -> Optional[str]:
for n in names:
if n in df.columns:
return n
return None
c_loc = pick(["locationId", "库位ID", "库位Id", "库位id"])
c_wh = pick(["warehouseId", "仓库ID", "仓库Id", "仓库id"])
c_qty = pick(["数量", "quantity"])
c_x = pick(["x", "X", "posX", "坐标X"])
c_y = pick(["y", "Y", "posY", "坐标Y"])
c_z = pick(["z", "Z", "posZ", "坐标Z"])
required = [c_loc, c_wh, c_qty, c_x, c_y, c_z]
if any(c is None for c in required):
raise KeyError("Excel 缺少必要列locationId/warehouseId/数量/x/y/z支持多别名至少要能匹配到")
def as_int(v, d=0):
try:
if pd.isna(v): return d
return int(v)
except Exception:
try:
return int(float(v))
except Exception:
return d
def as_float(v, d=0.0):
try:
if pd.isna(v): return d
return float(v)
except Exception:
return d
def as_str(v, d=""):
if v is None or (isinstance(v, float) and pd.isna(v)): return d
s = str(v).strip()
return s if s else d
items: List[Dict[str, Any]] = []
for _, row in df.iterrows():
items.append({
"locationId": as_str(row[c_loc]),
"warehouseId": as_str(row[c_wh]),
"quantity": as_float(row[c_qty]),
"x": as_int(row[c_x]),
"y": as_int(row[c_y]),
"z": as_int(row[c_z]),
})
return self._post_lims("/api/lims/storage/auto-batch-out-bound", items)
# 2.14 新建实验
def create_orders(self, xlsx_path: str) -> Dict[str, Any]:
"""
从 Excel 解析并创建实验2.14
约定:
- batchId = Excel 文件名(不含扩展名)
- 物料列:所有以 "(g)" 结尾(不再读取“总质量(g)”列)
- totalMass 自动计算为所有物料质量之和
- createTime 缺失或为空时自动填充为当前日期YYYY/M/D
"""
path = Path(xlsx_path)
if not path.exists():
raise FileNotFoundError(f"未找到 Excel 文件:{path}")
try:
df = pd.read_excel(path, sheet_name=0, engine="openpyxl")
except Exception as e:
raise RuntimeError(f"读取 Excel 失败:{e}")
# 列名容错:返回可选列名,找不到则返回 None
def _pick(col_names: List[str]) -> Optional[str]:
for c in col_names:
if c in df.columns:
return c
return None
col_order_name = _pick(["配方ID", "orderName", "订单编号"])
col_create_time = _pick(["创建日期", "createTime"])
col_bottle_type = _pick(["配液瓶类型", "bottleType"])
col_mix_time = _pick(["混匀时间(s)", "mixTime"])
col_load = _pick(["扣电组装分液体积", "loadSheddingInfo"])
col_pouch = _pick(["软包组装分液体积", "pouchCellInfo"])
col_cond = _pick(["电导测试分液体积", "conductivityInfo"])
col_cond_cnt = _pick(["电导测试分液瓶数", "conductivityBottleCount"])
# 物料列:所有以 (g) 结尾
material_cols = [c for c in df.columns if isinstance(c, str) and c.endswith("(g)")]
if not material_cols:
raise KeyError("未发现任何以“(g)”结尾的物料列,请检查表头。")
batch_id = path.stem
def _to_ymd_slash(v) -> str:
# 统一为 "YYYY/M/D";为空或解析失败则用当前日期
if v is None or (isinstance(v, float) and pd.isna(v)) or str(v).strip() == "":
ts = datetime.now()
else:
try:
ts = pd.to_datetime(v)
except Exception:
ts = datetime.now()
return f"{ts.year}/{ts.month}/{ts.day}"
def _as_int(val, default=0) -> int:
try:
if pd.isna(val):
return default
return int(val)
except Exception:
return default
def _as_str(val, default="") -> str:
if val is None or (isinstance(val, float) and pd.isna(val)):
return default
s = str(val).strip()
return s if s else default
orders: List[Dict[str, Any]] = []
for idx, row in df.iterrows():
mats: List[Dict[str, Any]] = []
total_mass = 0.0
for mcol in material_cols:
val = row.get(mcol, None)
if val is None or (isinstance(val, float) and pd.isna(val)):
continue
try:
mass = float(val)
except Exception:
continue
if mass > 0:
mats.append({"name": mcol.replace("(g)", ""), "mass": mass})
total_mass += mass
order_data = {
"batchId": batch_id,
"orderName": _as_str(row[col_order_name], default=f"{batch_id}_order_{idx+1}") if col_order_name else f"{batch_id}_order_{idx+1}",
"createTime": _to_ymd_slash(row[col_create_time]) if col_create_time else _to_ymd_slash(None),
"bottleType": _as_str(row[col_bottle_type], default="配液小瓶") if col_bottle_type else "配液小瓶",
"mixTime": _as_int(row[col_mix_time]) if col_mix_time else 0,
"loadSheddingInfo": _as_int(row[col_load]) if col_load else 0,
"pouchCellInfo": _as_int(row[col_pouch]) if col_pouch else 0,
"conductivityInfo": _as_int(row[col_cond]) if col_cond else 0,
"conductivityBottleCount": _as_int(row[col_cond_cnt]) if col_cond_cnt else 0,
"materialInfos": mats,
"totalMass": round(total_mass, 4) # 自动汇总
}
orders.append(order_data)
# print(orders)
while True:
time.sleep(5)
response = self._post_lims("/api/lims/order/orders", orders)
if response.get("data", []):
break
logger.info(f"等待配液实验创建完成")
# self.order_status[response["data"]["orderCode"]] = "running"
# while True:
# time.sleep(5)
# if self.order_status.get(response["data"]["orderCode"], None) == "finished":
# logger.info(f"配液实验已完成 ,即将执行 3-2-1 转运")
# break
# logger.info(f"等待配液实验完成")
# self.transfer_3_to_2_to_1()
# self.wait_for_transfer_task()
# logger.info(f"3-2-1 转运完成,返回结果")
# return r321
return response
# 2.7 启动调度
def scheduler_start(self) -> Dict[str, Any]:
response = self._post_lims("/api/lims/scheduler/start")
print(response)
return response
# 3.10 停止调度
def scheduler_stop(self) -> Dict[str, Any]:
"""
停止调度 (3.10)
请求体只包含 apiKey 和 requestTime
"""
return self._post_lims("/api/lims/scheduler/stop")
# 2.9 继续调度
def scheduler_continue(self) -> Dict[str, Any]:
"""
继续调度 (2.9)
请求体只包含 apiKey 和 requestTime
"""
return self._post_lims("/api/lims/scheduler/continue")
# 2.24 物料变更推送
def report_material_change(self, material_obj: Dict[str, Any]) -> Dict[str, Any]:
"""
material_obj 按 2.24 的裸对象格式(包含 id/typeName/locations/detail 等)
"""
return self._post_report_raw("/report/material_change", material_obj)
# 2.21 步骤完成推送BS → LIMS
def report_step_finish(self,
order_code: str,
order_name: str,
step_name: str,
step_id: str,
sample_id: str,
start_time: str,
end_time: str,
execution_status: str = "completed") -> Dict[str, Any]:
data = {
"orderCode": order_code,
"orderName": order_name,
"stepName": step_name,
"stepId": step_id,
"sampleId": sample_id,
"startTime": start_time,
"endTime": end_time,
"executionStatus": execution_status
}
return self._post_report("/report/step_finish", data)
# 2.23 订单完成推送BS → LIMS
def report_order_finish(self,
order_code: str,
order_name: str,
start_time: str,
end_time: str,
status: str = "30", # 30 完成 / -11 异常停止 / -12 人工停止
workflow_status: str = "Finished",
completion_time: Optional[str] = None,
used_materials: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]:
data = {
"orderCode": order_code,
"orderName": order_name,
"startTime": start_time,
"endTime": end_time,
"status": status,
"workflowStatus": workflow_status,
"completionTime": completion_time or end_time,
"usedMaterials": used_materials or []
}
return self._post_report("/report/order_finish", data)
# 2.5 批量查询实验报告(用于轮询是否完成)
def order_list(self,
status: Optional[str] = None,
begin_time: Optional[str] = None,
end_time: Optional[str] = None,
filter_text: Optional[str] = None,
skip: int = 0, page: int = 10) -> Dict[str, Any]:
data: Dict[str, Any] = {"skipCount": skip, "pageCount": page}
if status is not None: # 80 成功 / 90 失败 / 100 执行中
data["status"] = status
if begin_time:
data["timeType"] = "CreationTime"
data["beginTime"] = begin_time
if end_time:
data["endTime"] = end_time
if filter_text:
data["filter"] = filter_text
return self._post_lims("/api/lims/order/order-list", data)
# 2.6 实验报告查询根据任务ID拿详情
def order_report(self, order_id: str) -> Dict[str, Any]:
return self._post_lims("/api/lims/order/order-report", order_id)
# 2.32 3-2-1 物料转运
def transfer_3_to_2_to_1(self,
# source_wh_id: Optional[str] = None,
source_wh_id: Optional[str] = '3a19debc-84b4-0359-e2d4-b3beea49348b',
source_x: int = 1, source_y: int = 1, source_z: int = 1) -> Dict[str, Any]:
payload: Dict[str, Any] = {
"sourcePosX": source_x, "sourcePosY": source_y, "sourcePosZ": source_z
}
if source_wh_id:
payload["sourceWHID"] = source_wh_id
return self._post_lims("/api/lims/order/transfer-task3To2To1", payload)
# 2.28 样品/废料取出
def take_out(self,
order_id: str,
preintake_ids: Optional[List[str]] = None,
material_ids: Optional[List[str]] = None) -> Dict[str, Any]:
data = {
"orderId": order_id,
"preintakeIds": preintake_ids or [],
"materialIds": material_ids or []
}
return self._post_lims("/api/lims/order/take-out", data)
# --------可选占位方法文档未定义的“1号站内部流程 / 1-2转运”--------
def start_station1_internal_flow(self, **kwargs) -> None:
logger.info("启动1号站内部流程占位按现场系统填充具体指令")
# 3.x 1→2 物料转运
def transfer_1_to_2(self) -> Dict[str, Any]:
"""
1→2 物料转运
URL: /api/lims/order/transfer-task1To2
只需要 apiKey 和 requestTime
"""
return self._post_lims("/api/lims/order/transfer-task1To2")
# -------------------- 整体编排 --------------------
def run_full_workflow(self,
inbound_items: List[Dict[str, str]],
orders: List[Dict[str, Any]],
poll_filter_code: Optional[str] = None,
poll_timeout_s: int = 600,
poll_interval_s: int = 5,
transfer_source: Optional[Dict[str, Any]] = None,
takeout_order_id: Optional[str] = None) -> None:
"""
一键串联:
1) 入库 3-4 个物料 → 2) 新建实验 → 3) 启动调度
运行中如需4) 物料变更推送 5) 步骤完成推送 6) 订单完成推送
完成后查询实验2.5/2.6)→ 7) 3-2-1 转运 → 8) 1号站内部流程
→ 9) 1-2 转运 → 10) 样品/废料取出
"""
# 1. 入库多于1个就用批量接口 2.18
if len(inbound_items) == 1:
r = self.storage_inbound(inbound_items[0]["materialId"], inbound_items[0]["locationId"])
logger.info(f"单个入库结果: {r}")
else:
r = self.storage_batch_inbound(inbound_items)
logger.info(f"批量入库结果: {r}")
# 2. 新建实验2.14
r = self.create_orders(orders)
logger.info(f"新建实验结果: {r}")
# 3. 启动调度2.7
r = self.scheduler_start()
logger.info(f"启动调度结果: {r}")
# —— 运行中各类推送2.24 / 2.21 / 2.23),通常由实际任务驱动,这里提供调用方式 —— #
# self.report_material_change({...})
# self.report_step_finish(order_code="BSO...", order_name="配液分液", step_name="xxx", step_id="...", sample_id="...",
# start_time=_iso_utc_now_ms(), end_time=_iso_utc_now_ms(), execution_status="completed")
# self.report_order_finish(order_code="BSO...", order_name="配液分液", start_time="...", end_time=_iso_utc_now_ms())
# 完成后才能转运:用 2.5 批量查询配合 filter=任务编码 轮询到 status=80成功
if poll_filter_code:
import time
deadline = time.time() + poll_timeout_s
while time.time() < deadline:
res = self.order_list(status="80", filter_text=poll_filter_code, page=5)
if isinstance(res, dict) and res.get("data", {}).get("items"):
logger.info(f"实验 {poll_filter_code} 已完成:{res['data']['items'][0]}")
break
time.sleep(poll_interval_s)
else:
logger.warning(f"等待实验 {poll_filter_code} 完成超时(未到 status=80")
# 7. 启动 3-2-1 转运2.32
if transfer_source:
r = self.transfer_3_to_2_to_1(
source_wh_id=transfer_source.get("sourceWHID"),
source_x=transfer_source.get("sourcePosX", 1),
source_y=transfer_source.get("sourcePosY", 1),
source_z=transfer_source.get("sourcePosZ", 1),
)
logger.info(f"3-2-1 转运结果: {r}")
# 8. 1号站内部流程占位
self.start_station1_internal_flow()
# 9. 1→2 转运(占位)
self.transfer_1_to_2()
# 10. 样品/废料取出2.28
if takeout_order_id:
r = self.take_out(order_id=takeout_order_id)
logger.info(f"样品/废料取出结果: {r}")
# 2.5 批量查询实验报告
def order_list_v2(self,
timeType: str = "string",
beginTime: str = "",
endTime: str = "",
status: str = "",
filter: str = "",
skipCount: int = 0,
pageCount: int = 1,
sorting: str = "") -> Dict[str, Any]:
"""
批量查询实验报告的详细信息 (2.5)
URL: /api/lims/order/order-list
参数默认值和接口文档保持一致
"""
data: Dict[str, Any] = {
"timeType": timeType,
"beginTime": beginTime,
"endTime": endTime,
"status": status,
"filter": filter,
"skipCount": skipCount,
"pageCount": pageCount,
"sorting": sorting
}
return self._post_lims("/api/lims/order/order-list", data)
def wait_for_transfer_task(self, timeout: int = 3000, interval: int = 5, filter_text: Optional[str] = None) -> bool:
"""
轮询查询物料转移任务是否成功完成 (status=80)
- timeout: 最大等待秒数 (默认600秒)
- interval: 轮询间隔秒数 (默认3秒)
返回 True 表示找到并成功完成False 表示超时未找到
"""
now = datetime.now()
beginTime = now.strftime("%Y-%m-%dT%H:%M:%SZ")
endTime = (now + timedelta(minutes=5)).strftime("%Y-%m-%dT%H:%M:%SZ")
print(beginTime, endTime)
deadline = time.time() + timeout
while time.time() < deadline:
result = self.order_list_v2(
timeType="string",
beginTime=beginTime,
endTime=endTime,
status="",
filter=filter_text,
skipCount=0,
pageCount=1,
sorting=""
)
print(result)
items = result.get("data", {}).get("items", [])
for item in items:
name = item.get("name", "")
status = item.get("status")
# 改成用 filter_text 判断
if (not filter_text or filter_text in name) and status == 80:
logger.info(f"硬件转移动作完成: {name}, status={status}")
return True
logger.info(f"等待中: {name}, status={status}")
time.sleep(interval)
logger.warning("超时未找到成功的物料转移任务")
return False
def Bioystation_scheduler_start_task(self) -> bool:
logger.info("开始调度")
self.scheduler_start()
logger.info("调度已启动")
def Bioystation_scheduler_stop_task(self) -> bool:
logger.info("停止调度")
self.scheduler_stop()
logger.info("调度已停止")
def Bioystation_scheduler_continue_task(self) -> bool:
logger.info("继续调度")
self.scheduler_continue()
logger.info("调度已继续")
# 3.30 上料:读取模板 Excel 自动解析并 POST
def Bioystation_feeding4to3_from_xlsx_task(self) -> bool:
logger.info("4号箱自动上料开始")
r1 = self.auto_feeding4to3_from_xlsx(r"D:\Uni-lab\Uni-Lab-OS\unilabos\devices\workstation\bioyond_cell\样品导入模板.xlsx")
self.wait_for_transfer_task(filter_text="物料转移任务")
logger.info("4号箱向3号箱转运物料转移任务已完成")
return True
# # 新建实验
def Bioystation_start_experiment_task(self) -> bool:
logger.info("3号箱内实验开始")
response = self.create_orders(r"D:\Uni-lab\Uni-Lab-OS\unilabos\devices\workstation\bioyond_cell\2025101301.xlsx")
logger.info(response)
data_list = response.get("data", [])
order_name = data_list[0].get("orderName", "")
self.wait_for_transfer_task(filter_text=order_name)
logger.info("3号站内实验完成")
return True
def Bioystation_3_to_2_task(self) -> bool:
self.transfer_3_to_2_to_1()
self.wait_for_transfer_task(filter_text="物料转移任务")
logger.info("3号站向2号站向1号站转移任务完成")
return True
def Bioystation_1_to_2_task(self) -> bool:
self.transfer_1_to_2()
self.wait_for_transfer_task(filter_text="物料转移任务")
logger.info("1号站向2号站转移任务完成")
logger.info("全流程结束")
return True
def test_benyao_workstation(self, num1, num2):
num1 = int(num1)
num2 = int(num2)
for i in range(num1):
print(f"num1 = {num1}")
for j in range(num2):
print(f"num1 = {num2}")
# --------------------------------
if __name__ == "__main__":
ws = BioyondWorkstation()
#ws.scheduler_stop()
#ws.Bioystation_scheduler_start_task()
ws.scheduler_start()
# ws.scheduler_start()
# logger.info("调度启动完成")
# ws.scheduler_continue()
# 3.30 上料:读取模板 Excel 自动解析并 POST
# r1 = ws.auto_feeding4to3_from_xlsx(r"C:\ML\GitHub\Uni-Lab-OS\unilabos\devices\workstation\bioyond_cell\样品导入模板 (8).xlsx")
# ws.wait_for_transfer_task(filter_text="物料转移任务")
# logger.info("4号箱向3号箱转运物料转移任务已完成")
# ws.scheduler_start()
# print(r1["payload"]["data"]) # 调试模式下可直接看到要发的 JSON items
# # 新建实验
# response = ws.create_orders("C:/ML/GitHub/Uni-Lab-OS/unilabos/devices/workstation/bioyond_cell/2025092701.xlsx")
# logger.info(response)
# data_list = response.get("data", [])
# order_name = data_list[0].get("orderName", "")
# ws.wait_for_transfer_task(filter_text=order_name)
# ws.wait_for_transfer_task(filter_text='DP20250927001')
# logger.info("3号站内实验完成")
# # ws.scheduler_start()
# # print(res)
# ws.transfer_3_to_2_to_1()
# ws.wait_for_transfer_task(filter_text="物料转移任务")
# logger.info("3号站向2号站向1号站转移任务完成")
# r321 = self.wait_for_transfer_task()
#1号站启动
# ws.transfer_1_to_2()
#s.wait_for_transfer_task(filter_text="物料转移任务")
#ogger.info("1号站向2号站转移任务完成")
#ogger.info("全流程结束")
# 3.31 下料:同理
# r2 = ws.auto_batch_outbound_from_xlsx(r"C:/path/样品导入模板 (8).xlsx")
# print(r2["payload"]["data"])

View File

@@ -0,0 +1,772 @@
# -*- coding: utf-8 -*-
from typing import Dict, Any, List, Optional
from datetime import datetime, timezone
import requests
from pathlib import Path
import pandas as pd
import time
from datetime import datetime, timezone, timedelta
import re
import threading
from unilabos.devices.workstation.workstation_base import WorkstationBase
from unilabos.devices.workstation.workstation_http_service import WorkstationHTTPService
from unilabos.utils.log import logger
from pylabrobot.resources.deck import Deck
def _iso_utc_now_ms() -> str:
# 文档要求:到毫秒 + Z例如 2025-08-15T05:43:22.814Z
dt = datetime.now(timezone.utc)
return dt.strftime("%Y-%m-%dT%H:%M:%S.") + f"{int(dt.microsecond/1000):03d}Z"
class BioyondWorkstation(WorkstationBase):
"""
集成 Bioyond LIMS 的工作站示例,
覆盖:入库(2.17/2.18) → 新建实验(2.14) → 启动调度(2.7) →
运行中推送:物料变更(2.24)、步骤完成(2.21)、订单完成(2.23) →
查询实验(2.5/2.6) → 3-2-1 转运(2.32) → 样品/废料取出(2.28)
"""
def __init__(
self,
bioyond_config: Optional[Dict[str, Any]] = None,
station_resource: Optional[Dict[str, Any]] = None,
debug_mode: bool = False, # 增加调试模式开关
*args, **kwargs,
):
self.bioyond_config = bioyond_config or {
#"base_url": "http://192.168.1.200:44386",
#"base_url": "http://172.16.11.219:44388",
"base_url": "http://61.169.57.196:44422",
"api_key": "8A819E5C",
"timeout": 30,
"report_token": "CHANGE_ME_TOKEN"
}
self.debug_mode = debug_mode
super().__init__(deck=Deck, station_resource=station_resource, *args, **kwargs)
logger.info(f"Bioyond工作站初始化完成 (debug_mode={self.debug_mode})")
# 实例化并在后台线程启动 HTTP 报送服务
self.order_status = {}
try:
t = threading.Thread(target=self._start_http_service_bg, daemon=True, name="unilab_http")
t.start()
except Exception as e:
logger.error(f"unilab-server后台启动报送服务失败: {e}")
@property
def device_id(self) -> str:
try:
return getattr(self, "_ros_node").device_id # 兼容 ROS 场景
except Exception:
return "bioyond_workstation"
def _start_http_service_bg(self, host: str = "192.168.1.104", port: int = 7000) -> None:
try:
self.service = WorkstationHTTPService(self, host=host, port=port)
self.service.start()
except Exception as e:
logger.error(f"启动HTTP服务失败: {e}")
# -------------------- 基础HTTP封装 --------------------
def _url(self, path: str) -> str:
return f"{self.bioyond_config['base_url'].rstrip('/')}/{path.lstrip('/')}"
def _post_lims(self, path: str, data: Optional[Any] = None) -> Dict[str, Any]:
"""LIMS API大多数接口用 {apiKey/requestTime,data} 包装"""
payload = {
"apiKey": self.bioyond_config["api_key"],
"requestTime": _iso_utc_now_ms()
}
if data is not None:
payload["data"] = data
if self.debug_mode:
# 模拟返回,不发真实请求
logger.info(f"[DEBUG] POST {path} with payload={payload}")
return {"debug": True, "url": self._url(path), "payload": payload, "status": "ok"}
try:
r = requests.post(
self._url(path),
json=payload,
timeout=self.bioyond_config.get("timeout", 30),
headers={"Content-Type": "application/json"}
)
r.raise_for_status()
return r.json()
except Exception as e:
logger.error(f"POST {path} 失败: {e}")
return {"error": str(e)}
# --- 修正_post_report / _post_report_raw 同样走 debug_mode ---
def _post_report(self, path: str, data: Dict[str, Any]) -> Dict[str, Any]:
payload = {
"token": self.bioyond_config.get("report_token", ""),
"request_time": _iso_utc_now_ms(),
"data": data
}
if self.debug_mode:
logger.info(f"[DEBUG] POST {path} with payload={payload}")
return {"debug": True, "url": self._url(path), "payload": payload, "status": "ok"}
try:
r = requests.post(self._url(path), json=payload,
timeout=self.bioyond_config.get("timeout", 30),
headers={"Content-Type": "application/json"})
r.raise_for_status()
return r.json()
except Exception as e:
logger.error(f"POST {path} 失败: {e}")
return {"error": str(e)}
def _post_report_raw(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]:
if self.debug_mode:
logger.info(f"[DEBUG] POST {path} with body={body}")
return {"debug": True, "url": self._url(path), "payload": body, "status": "ok"}
try:
r = requests.post(self._url(path), json=body,
timeout=self.bioyond_config.get("timeout", 30),
headers={"Content-Type": "application/json"})
r.raise_for_status()
return r.json()
except Exception as e:
logger.error(f"POST {path} 失败: {e}")
return {"error": str(e)}
# -------------------- 单点接口封装 --------------------
# 2.17 入库物料(单个)
def storage_inbound(self, material_id: str, location_id: str) -> Dict[str, Any]:
return self._post_lims("/api/lims/storage/inbound", {
"materialId": material_id,
"locationId": location_id
})
# 2.18 批量入库(多个)
def storage_batch_inbound(self, items: List[Dict[str, str]]) -> Dict[str, Any]:
"""
items = [{"materialId": "...", "locationId": "..."}, ...]
"""
return self._post_lims("/api/lims/storage/batch-inbound", items)
# 3.30 自动化上料Excel -> JSON -> POST /api/lims/order/auto-feeding4to3
def auto_feeding4to3_from_xlsx(self, xlsx_path: str) -> Dict[str, Any]:
"""
根据固定模板解析 Excel
- 四号手套箱加样头面 (2-13行, 3-7列)
- 四号手套箱原液瓶面 (15-23行, 3-9列)
- 三号手套箱人工堆栈 (26-40行, 3-7列)
"""
path = Path(xlsx_path)
if not path.exists():
raise FileNotFoundError(f"未找到 Excel 文件:{path}")
try:
df = pd.read_excel(path, sheet_name=0, header=None, engine="openpyxl")
except Exception as e:
raise RuntimeError(f"读取 Excel 失败:{e}")
items: List[Dict[str, Any]] = []
# 四号手套箱 - 加样头面2-13行, 3-7列
for _, row in df.iloc[1:13, 2:7].iterrows():
item = {
"sourceWHName": "四号手套箱堆栈",
"posX": int(row[2]),
"posY": int(row[3]),
"posZ": int(row[4]),
"materialName": str(row[5]).strip() if pd.notna(row[5]) else "",
"quantity": float(row[6]) if pd.notna(row[6]) else 0.0,
}
if item["materialName"]:
items.append(item)
# 四号手套箱 - 原液瓶面15-23行, 3-9列
for _, row in df.iloc[14:23, 2:9].iterrows():
item = {
"sourceWHName": "四号手套箱堆栈",
"posX": int(row[2]),
"posY": int(row[3]),
"posZ": int(row[4]),
"materialName": str(row[5]).strip() if pd.notna(row[5]) else "",
"quantity": float(row[6]) if pd.notna(row[6]) else 0.0,
"materialType": str(row[7]).strip() if pd.notna(row[7]) else "",
"targetWH": str(row[8]).strip() if pd.notna(row[8]) else "",
}
if item["materialName"]:
items.append(item)
# 三号手套箱人工堆栈26-40行, 3-7列
for _, row in df.iloc[25:40, 2:7].iterrows():
item = {
"sourceWHName": "三号手套箱人工堆栈",
"posX": int(row[2]),
"posY": int(row[3]),
"posZ": int(row[4]),
"materialType": str(row[5]).strip() if pd.notna(row[5]) else "",
"materialId": str(row[6]).strip() if pd.notna(row[6]) else "",
"quantity": 1 # 默认数量1
}
if item["materialId"] or item["materialType"]:
items.append(item)
print("items", items)
return self._post_lims("/api/lims/order/auto-feeding4to3", items)
def auto_batch_outbound_from_xlsx(self, xlsx_path: str) -> Dict[str, Any]:
"""
3.31 自动化下料Excel -> JSON -> POST /api/lims/storage/auto-batch-out-bound
"""
path = Path(xlsx_path)
if not path.exists():
raise FileNotFoundError(f"未找到 Excel 文件:{path}")
try:
df = pd.read_excel(path, sheet_name=0, engine="openpyxl")
except Exception as e:
raise RuntimeError(f"读取 Excel 失败:{e}")
def pick(names: List[str]) -> Optional[str]:
for n in names:
if n in df.columns:
return n
return None
c_loc = pick(["locationId", "库位ID", "库位Id", "库位id"])
c_wh = pick(["warehouseId", "仓库ID", "仓库Id", "仓库id"])
c_qty = pick(["数量", "quantity"])
c_x = pick(["x", "X", "posX", "坐标X"])
c_y = pick(["y", "Y", "posY", "坐标Y"])
c_z = pick(["z", "Z", "posZ", "坐标Z"])
required = [c_loc, c_wh, c_qty, c_x, c_y, c_z]
if any(c is None for c in required):
raise KeyError("Excel 缺少必要列locationId/warehouseId/数量/x/y/z支持多别名至少要能匹配到")
def as_int(v, d=0):
try:
if pd.isna(v): return d
return int(v)
except Exception:
try:
return int(float(v))
except Exception:
return d
def as_float(v, d=0.0):
try:
if pd.isna(v): return d
return float(v)
except Exception:
return d
def as_str(v, d=""):
if v is None or (isinstance(v, float) and pd.isna(v)): return d
s = str(v).strip()
return s if s else d
items: List[Dict[str, Any]] = []
for _, row in df.iterrows():
items.append({
"locationId": as_str(row[c_loc]),
"warehouseId": as_str(row[c_wh]),
"quantity": as_float(row[c_qty]),
"x": as_int(row[c_x]),
"y": as_int(row[c_y]),
"z": as_int(row[c_z]),
})
return self._post_lims("/api/lims/storage/auto-batch-out-bound", items)
# 2.14 新建实验
def create_orders(self, xlsx_path: str) -> Dict[str, Any]:
"""
从 Excel 解析并创建实验2.14
约定:
- batchId = Excel 文件名(不含扩展名)
- 物料列:所有以 "(g)" 结尾(不再读取“总质量(g)”列)
- totalMass 自动计算为所有物料质量之和
- createTime 缺失或为空时自动填充为当前日期YYYY/M/D
"""
path = Path(xlsx_path)
if not path.exists():
raise FileNotFoundError(f"未找到 Excel 文件:{path}")
try:
df = pd.read_excel(path, sheet_name=0, engine="openpyxl")
except Exception as e:
raise RuntimeError(f"读取 Excel 失败:{e}")
# 列名容错:返回可选列名,找不到则返回 None
def _pick(col_names: List[str]) -> Optional[str]:
for c in col_names:
if c in df.columns:
return c
return None
col_order_name = _pick(["配方ID", "orderName", "订单编号"])
col_create_time = _pick(["创建日期", "createTime"])
col_bottle_type = _pick(["配液瓶类型", "bottleType"])
col_mix_time = _pick(["混匀时间(s)", "mixTime"])
col_load = _pick(["扣电组装分液体积", "loadSheddingInfo"])
col_pouch = _pick(["软包组装分液体积", "pouchCellInfo"])
col_cond = _pick(["电导测试分液体积", "conductivityInfo"])
col_cond_cnt = _pick(["电导测试分液瓶数", "conductivityBottleCount"])
# 物料列:所有以 (g) 结尾
material_cols = [c for c in df.columns if isinstance(c, str) and c.endswith("(g)")]
if not material_cols:
raise KeyError("未发现任何以“(g)”结尾的物料列,请检查表头。")
batch_id = path.stem
def _to_ymd_slash(v) -> str:
# 统一为 "YYYY/M/D";为空或解析失败则用当前日期
if v is None or (isinstance(v, float) and pd.isna(v)) or str(v).strip() == "":
ts = datetime.now()
else:
try:
ts = pd.to_datetime(v)
except Exception:
ts = datetime.now()
return f"{ts.year}/{ts.month}/{ts.day}"
def _as_int(val, default=0) -> int:
try:
if pd.isna(val):
return default
return int(val)
except Exception:
return default
def _as_str(val, default="") -> str:
if val is None or (isinstance(val, float) and pd.isna(val)):
return default
s = str(val).strip()
return s if s else default
orders: List[Dict[str, Any]] = []
for idx, row in df.iterrows():
mats: List[Dict[str, Any]] = []
total_mass = 0.0
for mcol in material_cols:
val = row.get(mcol, None)
if val is None or (isinstance(val, float) and pd.isna(val)):
continue
try:
mass = float(val)
except Exception:
continue
if mass > 0:
mats.append({"name": mcol.replace("(g)", ""), "mass": mass})
total_mass += mass
order_data = {
"batchId": batch_id,
"orderName": _as_str(row[col_order_name], default=f"{batch_id}_order_{idx+1}") if col_order_name else f"{batch_id}_order_{idx+1}",
"createTime": _to_ymd_slash(row[col_create_time]) if col_create_time else _to_ymd_slash(None),
"bottleType": _as_str(row[col_bottle_type], default="配液小瓶") if col_bottle_type else "配液小瓶",
"mixTime": _as_int(row[col_mix_time]) if col_mix_time else 0,
"loadSheddingInfo": _as_int(row[col_load]) if col_load else 0,
"pouchCellInfo": _as_int(row[col_pouch]) if col_pouch else 0,
"conductivityInfo": _as_int(row[col_cond]) if col_cond else 0,
"conductivityBottleCount": _as_int(row[col_cond_cnt]) if col_cond_cnt else 0,
"materialInfos": mats,
"totalMass": round(total_mass, 4) # 自动汇总
}
orders.append(order_data)
# print(orders)
response = self._post_lims("/api/lims/order/orders", orders)
print(response["data"])
self.order_status[response["data"][0]["orderCode"]] = "running"
while True:
time.sleep(5)
if self.order_status.get(response["data"][0]["orderCode"], None) == "finished":
break
return response
# 2.7 启动调度
def scheduler_start(self) -> Dict[str, Any]:
return self._post_lims("/api/lims/scheduler/start")
# 3.10 停止调度
def scheduler_stop(self) -> Dict[str, Any]:
"""
停止调度 (3.10)
请求体只包含 apiKey 和 requestTime
"""
return self._post_lims("/api/lims/scheduler/stop")
# 2.9 继续调度
def scheduler_continue(self) -> Dict[str, Any]:
"""
继续调度 (2.9)
请求体只包含 apiKey 和 requestTime
"""
return self._post_lims("/api/lims/scheduler/continue")
# 2.24 物料变更推送
def report_material_change(self, material_obj: Dict[str, Any]) -> Dict[str, Any]:
"""
material_obj 按 2.24 的裸对象格式(包含 id/typeName/locations/detail 等)
"""
return self._post_report_raw("/report/material_change", material_obj)
# 2.21 步骤完成推送BS → LIMS
def report_step_finish(self,
order_code: str,
order_name: str,
step_name: str,
step_id: str,
sample_id: str,
start_time: str,
end_time: str,
execution_status: str = "completed") -> Dict[str, Any]:
data = {
"orderCode": order_code,
"orderName": order_name,
"stepName": step_name,
"stepId": step_id,
"sampleId": sample_id,
"startTime": start_time,
"endTime": end_time,
"executionStatus": execution_status
}
return self._post_report("/report/step_finish", data)
# 2.23 订单完成推送BS → LIMS
def report_order_finish(self,
order_code: str,
order_name: str,
start_time: str,
end_time: str,
status: str = "30", # 30 完成 / -11 异常停止 / -12 人工停止
workflow_status: str = "Finished",
completion_time: Optional[str] = None,
used_materials: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]:
data = {
"orderCode": order_code,
"orderName": order_name,
"startTime": start_time,
"endTime": end_time,
"status": status,
"workflowStatus": workflow_status,
"completionTime": completion_time or end_time,
"usedMaterials": used_materials or []
}
return self._post_report("/report/order_finish", data)
# 2.5 批量查询实验报告(用于轮询是否完成)
def order_list(self,
status: Optional[str] = None,
begin_time: Optional[str] = None,
end_time: Optional[str] = None,
filter_text: Optional[str] = None,
skip: int = 0, page: int = 10) -> Dict[str, Any]:
data: Dict[str, Any] = {"skipCount": skip, "pageCount": page}
if status is not None: # 80 成功 / 90 失败 / 100 执行中
data["status"] = status
if begin_time:
data["timeType"] = "CreationTime"
data["beginTime"] = begin_time
if end_time:
data["endTime"] = end_time
if filter_text:
data["filter"] = filter_text
return self._post_lims("/api/lims/order/order-list", data)
# 2.6 实验报告查询根据任务ID拿详情
def order_report(self, order_id: str) -> Dict[str, Any]:
return self._post_lims("/api/lims/order/order-report", order_id)
# 2.32 3-2-1 物料转运
def transfer_3_to_2_to_1(self,
# source_wh_id: Optional[str] = None,
source_wh_id: Optional[str] = '3a19debc-84b4-0359-e2d4-b3beea49348b',
source_x: int = 1, source_y: int = 1, source_z: int = 1) -> Dict[str, Any]:
payload: Dict[str, Any] = {
"sourcePosX": source_x, "sourcePosY": source_y, "sourcePosZ": source_z
}
if source_wh_id:
payload["sourceWHID"] = source_wh_id
return self._post_lims("/api/lims/order/transfer-task3To2To1", payload)
# 2.28 样品/废料取出
def take_out(self,
order_id: str,
preintake_ids: Optional[List[str]] = None,
material_ids: Optional[List[str]] = None) -> Dict[str, Any]:
data = {
"orderId": order_id,
"preintakeIds": preintake_ids or [],
"materialIds": material_ids or []
}
return self._post_lims("/api/lims/order/take-out", data)
# --------可选占位方法文档未定义的“1号站内部流程 / 1-2转运”--------
def start_station1_internal_flow(self, **kwargs) -> None:
logger.info("启动1号站内部流程占位按现场系统填充具体指令")
# 3.x 1→2 物料转运
def transfer_1_to_2(self) -> Dict[str, Any]:
"""
1→2 物料转运
URL: /api/lims/order/transfer-task1To2
只需要 apiKey 和 requestTime
"""
return self._post_lims("/api/lims/order/transfer-task1To2")
# -------------------- 整体编排 --------------------
def run_full_workflow(self,
inbound_items: List[Dict[str, str]],
orders: List[Dict[str, Any]],
poll_filter_code: Optional[str] = None,
poll_timeout_s: int = 600,
poll_interval_s: int = 5,
transfer_source: Optional[Dict[str, Any]] = None,
takeout_order_id: Optional[str] = None) -> None:
"""
一键串联:
1) 入库 3-4 个物料 → 2) 新建实验 → 3) 启动调度
运行中如需4) 物料变更推送 5) 步骤完成推送 6) 订单完成推送
完成后查询实验2.5/2.6)→ 7) 3-2-1 转运 → 8) 1号站内部流程
→ 9) 1-2 转运 → 10) 样品/废料取出
"""
# 1. 入库多于1个就用批量接口 2.18
if len(inbound_items) == 1:
r = self.storage_inbound(inbound_items[0]["materialId"], inbound_items[0]["locationId"])
logger.info(f"单个入库结果: {r}")
else:
r = self.storage_batch_inbound(inbound_items)
logger.info(f"批量入库结果: {r}")
# 2. 新建实验2.14
r = self.create_orders(orders)
logger.info(f"新建实验结果: {r}")
# 3. 启动调度2.7
r = self.scheduler_start()
logger.info(f"启动调度结果: {r}")
# —— 运行中各类推送2.24 / 2.21 / 2.23),通常由实际任务驱动,这里提供调用方式 —— #
# self.report_material_change({...})
# self.report_step_finish(order_code="BSO...", order_name="配液分液", step_name="xxx", step_id="...", sample_id="...",
# start_time=_iso_utc_now_ms(), end_time=_iso_utc_now_ms(), execution_status="completed")
# self.report_order_finish(order_code="BSO...", order_name="配液分液", start_time="...", end_time=_iso_utc_now_ms())
# 完成后才能转运:用 2.5 批量查询配合 filter=任务编码 轮询到 status=80成功
if poll_filter_code:
import time
deadline = time.time() + poll_timeout_s
while time.time() < deadline:
res = self.order_list(status="80", filter_text=poll_filter_code, page=5)
if isinstance(res, dict) and res.get("data", {}).get("items"):
logger.info(f"实验 {poll_filter_code} 已完成:{res['data']['items'][0]}")
break
time.sleep(poll_interval_s)
else:
logger.warning(f"等待实验 {poll_filter_code} 完成超时(未到 status=80")
# 7. 启动 3-2-1 转运2.32
if transfer_source:
r = self.transfer_3_to_2_to_1(
source_wh_id=transfer_source.get("sourceWHID"),
source_x=transfer_source.get("sourcePosX", 1),
source_y=transfer_source.get("sourcePosY", 1),
source_z=transfer_source.get("sourcePosZ", 1),
)
logger.info(f"3-2-1 转运结果: {r}")
# 8. 1号站内部流程占位
self.start_station1_internal_flow()
# 9. 1→2 转运(占位)
self.transfer_1_to_2()
# 10. 样品/废料取出2.28
if takeout_order_id:
r = self.take_out(order_id=takeout_order_id)
logger.info(f"样品/废料取出结果: {r}")
# 2.5 批量查询实验报告
def order_list_v2(self,
timeType: str = "string",
beginTime: str = "",
endTime: str = "",
status: str = "",
filter: str = "物料转移任务",
skipCount: int = 0,
pageCount: int = 1,
sorting: str = "") -> Dict[str, Any]:
"""
批量查询实验报告的详细信息 (2.5)
URL: /api/lims/order/order-list
参数默认值和接口文档保持一致
"""
data: Dict[str, Any] = {
"timeType": timeType,
"beginTime": beginTime,
"endTime": endTime,
"status": status,
"filter": filter,
"skipCount": skipCount,
"pageCount": pageCount,
"sorting": sorting
}
return self._post_lims("/api/lims/order/order-list", data)
def wait_for_transfer_task(self, timeout: int = 600, interval: int = 3) -> bool:
"""
轮询查询物料转移任务是否成功完成 (status=80)
- timeout: 最大等待秒数 (默认600秒)
- interval: 轮询间隔秒数 (默认3秒)
返回 True 表示找到并成功完成False 表示超时未找到
"""
now = datetime.now()
beginTime = now.strftime("%Y-%m-%dT%H:%M:%SZ")
endTime = (now + timedelta(minutes=5)).strftime("%Y-%m-%dT%H:%M:%SZ")
print(beginTime, endTime)
deadline = time.time() + timeout
while time.time() < deadline:
result = self.order_list_v2(
timeType="string",
beginTime=beginTime,
endTime=endTime,
status="",
filter="物料转移任务",
skipCount=0,
pageCount=1,
sorting=""
)
print(result)
items = result.get("data", {}).get("items", [])
for item in items:
name = item.get("name", "")
status = item.get("status")
if name.startswith("物料转移任务") and status == 80:
logger.info(f"硬件转移动作完成: {name}")
return True
time.sleep(interval)
logger.warning("超时未找到成功的物料转移任务")
return False
def wait_for_recent_task(self, timeout: int = 600, interval: int = 3) -> bool:
"""
轮询查询最近的任务是否成功完成 (status=80)
- timeout: 最大等待秒数 (默认600秒)
- interval: 轮询间隔秒数 (默认3秒)
返回 True 表示找到并成功完成False 表示超时未找到
"""
now = datetime.now()
beginTime = now.strftime("%Y-%m-%dT%H:%M:%SZ")
endTime = (now + timedelta(minutes=5)).strftime("%Y-%m-%dT%H:%M:%SZ")
print(beginTime, endTime)
deadline = time.time() + timeout
while time.time() < deadline:
result = self.order_list_v2(
timeType="string",
beginTime=beginTime,
endTime=endTime,
status="",
filter="",
skipCount=0,
pageCount=1,
sorting=""
)
print(result)
items = result.get("data", {}).get("items", [])
for item in items:
name = item.get("name", "")
status = item.get("status")
if name.startswith("物料转移任务") and status == 80:
logger.info(f"硬件转移动作完成: {name}")
return True
time.sleep(interval)
logger.warning("超时未找到成功的任务")
return False
# --------------------------------
if __name__ == "__main__":
ws = BioyondWorkstation()
ws.scheduler_stop()
ws.scheduler_start()
#物料入库
r1 = ws.auto_feeding4to3_from_xlsx(r"D:\Uni-lab\Uni-Lab-OS\unilabos\devices\workstation\bioyond_cell\样品导入模板.xlsx")
# print(r1)
# print("物料入库任务已提交0")
# #等待任务完成
# ws.wait_for_transfer_task()
#
# print("物料入库任务已完成1")
#
# #新建实验
# res = ws.create_orders(r"D:\Uni-lab\Uni-Lab-OS\unilabos\devices\workstation\bioyond_cell\2025092501.xlsx")
# print(res)
# #等待任务完成
# ws.wait_for_recent_task()
# print("配液任务已完成")
#
# #新建3-2-1转运任务
# r321 = ws.transfer_3_to_2_to_1()
# print(r321)
# #等待任务完成
# ws.wait_for_recent_task()
#
# ws.transfer_1_to_2()
# #等待任务完成
# ws.wait_for_recent_task()
#ws._start_http_service_bg()
# ws.scheduler_stop()
#ws.scheduler_start()
# ws.scheduler_continue()
# 3.30 上料:读取模板 Excel 自动解析并 POST
# r1 = ws.auto_feeding4to3_from_xlsx(r"C:\ML\GitHub\Uni-Lab-OS\unilabos\devices\workstation\bioyond_cell\样品导入模板 (8).xlsx")
#ws.wait_for_transfer_task()
#print("转运物料转移任务已完成")
# ws.scheduler_start()
# print(r1["payload"]["data"]) # 调试模式下可直接看到要发的 JSON items
# 新建实验
# res = ws.create_orders("C:/ML/GitHub/Uni-Lab-OS/unilabos/devices/workstation/bioyond_cell/2025092501.xlsx")
# ws.scheduler_start()
# print(res)
# r321 = ws.transfer_3_to_2_to_1()
# 3.31 下料:同理
# r2 = ws.auto_batch_outbound_from_xlsx(r"C:/path/样品导入模板 (8).xlsx")
# print(r2["payload"]["data"])
# r321 = ws.transfer_3_to_2_to_1()
# ws.transfer_1_to_2()

View File

@@ -0,0 +1,644 @@
# -*- coding: utf-8 -*-
from typing import Dict, Any, List, Optional
from datetime import datetime, timezone
import requests
from pathlib import Path
import pandas as pd
import time
import threading
from unilabos.devices.workstation.workstation_base import WorkstationBase
from unilabos.devices.workstation.workstation_http_service import WorkstationHTTPService
from unilabos.utils.log import logger
from pylabrobot.resources.deck import Deck
from .benyao_test import test_benyao_api
def _iso_utc_now_ms() -> str:
# 文档要求:到毫秒 + Z例如 2025-08-15T05:43:22.814Z
dt = datetime.now(timezone.utc)
return dt.strftime("%Y-%m-%dT%H:%M:%S.") + f"{int(dt.microsecond/1000):03d}Z"
class BioyondWorkstation(WorkstationBase):
"""
集成 Bioyond LIMS 的工作站示例,
覆盖:入库(2.17/2.18) → 新建实验(2.14) → 启动调度(2.7) →
运行中推送:物料变更(2.24)、步骤完成(2.21)、订单完成(2.23) →
查询实验(2.5/2.6) → 3-2-1 转运(2.32) → 样品/废料取出(2.28)
"""
def __init__(
self,
bioyond_config: Optional[Dict[str, Any]] = None,
station_resource: Optional[Dict[str, Any]] = None,
debug_mode: bool = False, # 增加调试模式开关
*args, **kwargs,
):
self.bioyond_config = bioyond_config or {
"base_url": "http://192.168.1.200:44386",
"api_key": "8A819E5C",
"timeout": 30,
"report_token": "CHANGE_ME_TOKEN"
}
self.debug_mode = debug_mode
super().__init__(deck=Deck, station_resource=station_resource, *args, **kwargs)
logger.info(f"Bioyond工作站初始化完成 (debug_mode={self.debug_mode})")
# 实例化并在后台线程启动 HTTP 报送服务
self.order_status = {}
try:
t = threading.Thread(target=self._start_http_service_bg, daemon=True, name="unilab_http")
t.start()
except Exception as e:
logger.error(f"unilab-server后台启动报送服务失败: {e}")
@property
def device_id(self) -> str:
try:
return getattr(self, "_ros_node").device_id # 兼容 ROS 场景
except Exception:
return "bioyond_workstation"
def _start_http_service_bg(self, host: str = "127.0.0.1", port: int = 8080) -> None:
try:
self.service = WorkstationHTTPService(self, host=host, port=port)
self.service.start()
except Exception as e:
logger.error(f"启动HTTP服务失败: {e}")
# -------------------- 基础HTTP封装 --------------------
def _url(self, path: str) -> str:
return f"{self.bioyond_config['base_url'].rstrip('/')}/{path.lstrip('/')}"
def _post_lims(self, path: str, data: Optional[Any] = None) -> Dict[str, Any]:
"""LIMS API大多数接口用 {apiKey/requestTime,data} 包装"""
payload = {
"apiKey": self.bioyond_config["api_key"],
"requestTime": _iso_utc_now_ms()
}
if data is not None:
payload["data"] = data
if self.debug_mode:
# 模拟返回,不发真实请求
logger.info(f"[DEBUG] POST {path} with payload={payload}")
return {"debug": True, "url": self._url(path), "payload": payload, "status": "ok"}
try:
r = requests.post(
self._url(path),
json=payload,
timeout=self.bioyond_config.get("timeout", 30),
headers={"Content-Type": "application/json"}
)
r.raise_for_status()
return r.json()
except Exception as e:
logger.error(f"POST {path} 失败: {e}")
return {"error": str(e)}
# --- 修正_post_report / _post_report_raw 同样走 debug_mode ---
def _post_report(self, path: str, data: Dict[str, Any]) -> Dict[str, Any]:
payload = {
"token": self.bioyond_config.get("report_token", ""),
"request_time": _iso_utc_now_ms(),
"data": data
}
if self.debug_mode:
logger.info(f"[DEBUG] POST {path} with payload={payload}")
return {"debug": True, "url": self._url(path), "payload": payload, "status": "ok"}
try:
r = requests.post(self._url(path), json=payload,
timeout=self.bioyond_config.get("timeout", 30),
headers={"Content-Type": "application/json"})
r.raise_for_status()
return r.json()
except Exception as e:
logger.error(f"POST {path} 失败: {e}")
return {"error": str(e)}
def _post_report_raw(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]:
if self.debug_mode:
logger.info(f"[DEBUG] POST {path} with body={body}")
return {"debug": True, "url": self._url(path), "payload": body, "status": "ok"}
try:
r = requests.post(self._url(path), json=body,
timeout=self.bioyond_config.get("timeout", 30),
headers={"Content-Type": "application/json"})
r.raise_for_status()
return r.json()
except Exception as e:
logger.error(f"POST {path} 失败: {e}")
return {"error": str(e)}
# -------------------- 单点接口封装 --------------------
# 2.17 入库物料(单个)
def storage_inbound(self, material_id: str, location_id: str) -> Dict[str, Any]:
return self._post_lims("/api/lims/storage/inbound", {
"materialId": material_id,
"locationId": location_id
})
# 2.18 批量入库(多个)
def storage_batch_inbound(self, items: List[Dict[str, str]]) -> Dict[str, Any]:
"""
items = [{"materialId": "...", "locationId": "..."}, ...]
"""
return self._post_lims("/api/lims/storage/batch-inbound", items)
# 3.30 自动化上料Excel -> JSON -> POST /api/lims/order/auto-feeding4to3
def auto_feeding4to3_from_xlsx(self, xlsx_path: str) -> Dict[str, Any]:
"""
根据固定模板解析 Excel
- 四号手套箱加样头面 (2-13行, 3-7列)
- 四号手套箱原液瓶面 (15-23行, 3-9列)
- 三号手套箱人工堆栈 (26-40行, 3-7列)
"""
path = Path(xlsx_path)
if not path.exists():
raise FileNotFoundError(f"未找到 Excel 文件:{path}")
try:
df = pd.read_excel(path, sheet_name=0, header=None, engine="openpyxl")
except Exception as e:
raise RuntimeError(f"读取 Excel 失败:{e}")
items: List[Dict[str, Any]] = []
# 四号手套箱 - 加样头面2-13行, 3-7列
for _, row in df.iloc[1:13, 2:7].iterrows():
item = {
"sourceWHName": "四号手套箱堆栈",
"posX": int(row[2]),
"posY": int(row[3]),
"posZ": int(row[4]),
"materialName": str(row[5]).strip() if pd.notna(row[5]) else "",
"quantity": float(row[6]) if pd.notna(row[6]) else 0.0,
}
if item["materialName"]:
items.append(item)
# 四号手套箱 - 原液瓶面15-23行, 3-9列
for _, row in df.iloc[14:23, 2:9].iterrows():
item = {
"sourceWHName": "四号手套箱堆栈",
"posX": int(row[2]),
"posY": int(row[3]),
"posZ": int(row[4]),
"materialName": str(row[5]).strip() if pd.notna(row[5]) else "",
"quantity": float(row[6]) if pd.notna(row[6]) else 0.0,
"materialType": str(row[7]).strip() if pd.notna(row[7]) else "",
"targetWH": str(row[8]).strip() if pd.notna(row[8]) else "",
}
if item["materialName"]:
items.append(item)
# 三号手套箱人工堆栈26-40行, 3-7列
for _, row in df.iloc[25:40, 2:7].iterrows():
item = {
"sourceWHName": "三号手套箱人工堆栈",
"posX": int(row[2]),
"posY": int(row[3]),
"posZ": int(row[4]),
"materialType": str(row[5]).strip() if pd.notna(row[5]) else "",
"materialId": str(row[6]).strip() if pd.notna(row[6]) else "",
"quantity": 1 # 默认数量1
}
if item["materialId"] or item["materialType"]:
items.append(item)
return self._post_lims("/api/lims/order/auto-feeding4to3", items)
def auto_batch_outbound_from_xlsx(self, xlsx_path: str) -> Dict[str, Any]:
"""
3.31 自动化下料Excel -> JSON -> POST /api/lims/storage/auto-batch-out-bound
"""
path = Path(xlsx_path)
if not path.exists():
raise FileNotFoundError(f"未找到 Excel 文件:{path}")
try:
df = pd.read_excel(path, sheet_name=0, engine="openpyxl")
except Exception as e:
raise RuntimeError(f"读取 Excel 失败:{e}")
def pick(names: List[str]) -> Optional[str]:
for n in names:
if n in df.columns:
return n
return None
c_loc = pick(["locationId", "库位ID", "库位Id", "库位id"])
c_wh = pick(["warehouseId", "仓库ID", "仓库Id", "仓库id"])
c_qty = pick(["数量", "quantity"])
c_x = pick(["x", "X", "posX", "坐标X"])
c_y = pick(["y", "Y", "posY", "坐标Y"])
c_z = pick(["z", "Z", "posZ", "坐标Z"])
required = [c_loc, c_wh, c_qty, c_x, c_y, c_z]
if any(c is None for c in required):
raise KeyError("Excel 缺少必要列locationId/warehouseId/数量/x/y/z支持多别名至少要能匹配到")
def as_int(v, d=0):
try:
if pd.isna(v): return d
return int(v)
except Exception:
try:
return int(float(v))
except Exception:
return d
def as_float(v, d=0.0):
try:
if pd.isna(v): return d
return float(v)
except Exception:
return d
def as_str(v, d=""):
if v is None or (isinstance(v, float) and pd.isna(v)): return d
s = str(v).strip()
return s if s else d
items: List[Dict[str, Any]] = []
for _, row in df.iterrows():
items.append({
"locationId": as_str(row[c_loc]),
"warehouseId": as_str(row[c_wh]),
"quantity": as_float(row[c_qty]),
"x": as_int(row[c_x]),
"y": as_int(row[c_y]),
"z": as_int(row[c_z]),
})
return self._post_lims("/api/lims/storage/auto-batch-out-bound", items)
# 2.14 新建实验
def create_orders(self, xlsx_path: str) -> Dict[str, Any]:
"""
从 Excel 解析并创建实验2.14
约定:
- batchId = Excel 文件名(不含扩展名)
- 物料列:所有以 "(g)" 结尾(不再读取“总质量(g)”列)
- totalMass 自动计算为所有物料质量之和
- createTime 缺失或为空时自动填充为当前日期YYYY/M/D
"""
path = Path(xlsx_path)
if not path.exists():
raise FileNotFoundError(f"未找到 Excel 文件:{path}")
try:
df = pd.read_excel(path, sheet_name=0, engine="openpyxl")
except Exception as e:
raise RuntimeError(f"读取 Excel 失败:{e}")
# 列名容错:返回可选列名,找不到则返回 None
def _pick(col_names: List[str]) -> Optional[str]:
for c in col_names:
if c in df.columns:
return c
return None
col_order_name = _pick(["配方ID", "orderName", "订单编号"])
col_create_time = _pick(["创建日期", "createTime"])
col_bottle_type = _pick(["配液瓶类型", "bottleType"])
col_mix_time = _pick(["混匀时间(s)", "mixTime"])
col_load = _pick(["扣电组装分液体积", "loadSheddingInfo"])
col_pouch = _pick(["软包组装分液体积", "pouchCellInfo"])
col_cond = _pick(["电导测试分液体积", "conductivityInfo"])
col_cond_cnt = _pick(["电导测试分液瓶数", "conductivityBottleCount"])
# 物料列:所有以 (g) 结尾
material_cols = [c for c in df.columns if isinstance(c, str) and c.endswith("(g)")]
if not material_cols:
raise KeyError("未发现任何以“(g)”结尾的物料列,请检查表头。")
batch_id = path.stem
def _to_ymd_slash(v) -> str:
# 统一为 "YYYY/M/D";为空或解析失败则用当前日期
if v is None or (isinstance(v, float) and pd.isna(v)) or str(v).strip() == "":
ts = datetime.now()
else:
try:
ts = pd.to_datetime(v)
except Exception:
ts = datetime.now()
return f"{ts.year}/{ts.month}/{ts.day}"
def _as_int(val, default=0) -> int:
try:
if pd.isna(val):
return default
return int(val)
except Exception:
return default
def _as_str(val, default="") -> str:
if val is None or (isinstance(val, float) and pd.isna(val)):
return default
s = str(val).strip()
return s if s else default
orders: List[Dict[str, Any]] = []
for idx, row in df.iterrows():
mats: List[Dict[str, Any]] = []
total_mass = 0.0
for mcol in material_cols:
val = row.get(mcol, None)
if val is None or (isinstance(val, float) and pd.isna(val)):
continue
try:
mass = float(val)
except Exception:
continue
if mass > 0:
mats.append({"name": mcol.replace("(g)", ""), "mass": mass})
total_mass += mass
order_data = {
"batchId": batch_id,
"orderName": _as_str(row[col_order_name], default=f"{batch_id}_order_{idx+1}") if col_order_name else f"{batch_id}_order_{idx+1}",
"createTime": _to_ymd_slash(row[col_create_time]) if col_create_time else _to_ymd_slash(None),
"bottleType": _as_str(row[col_bottle_type], default="配液小瓶") if col_bottle_type else "配液小瓶",
"mixTime": _as_int(row[col_mix_time]) if col_mix_time else 0,
"loadSheddingInfo": _as_int(row[col_load]) if col_load else 0,
"pouchCellInfo": _as_int(row[col_pouch]) if col_pouch else 0,
"conductivityInfo": _as_int(row[col_cond]) if col_cond else 0,
"conductivityBottleCount": _as_int(row[col_cond_cnt]) if col_cond_cnt else 0,
"materialInfos": mats,
"totalMass": round(total_mass, 4) # 自动汇总
}
orders.append(order_data)
# print(orders)
response = self._post_lims("/api/lims/order/orders", orders)
self.order_status[response["data"]["orderCode"]] = "running"
while True:
time.sleep(5)
if self.order_status.get(response["data"]["orderCode"], None) == "finished":
break
return response
# 2.7 启动调度
def scheduler_start(self) -> Dict[str, Any]:
return self._post_lims("/api/lims/scheduler/start")
# 3.10 停止调度
def scheduler_stop(self) -> Dict[str, Any]:
"""
停止调度 (3.10)
请求体只包含 apiKey 和 requestTime
"""
return self._post_lims("/api/lims/scheduler/stop")
# 2.24 物料变更推送
def report_material_change(self, material_obj: Dict[str, Any]) -> Dict[str, Any]:
"""
material_obj 按 2.24 的裸对象格式(包含 id/typeName/locations/detail 等)
"""
return self._post_report_raw("/report/material_change", material_obj)
# 2.21 步骤完成推送BS → LIMS
def report_step_finish(self,
order_code: str,
order_name: str,
step_name: str,
step_id: str,
sample_id: str,
start_time: str,
end_time: str,
execution_status: str = "completed") -> Dict[str, Any]:
data = {
"orderCode": order_code,
"orderName": order_name,
"stepName": step_name,
"stepId": step_id,
"sampleId": sample_id,
"startTime": start_time,
"endTime": end_time,
"executionStatus": execution_status
}
return self._post_report("/report/step_finish", data)
# 2.23 订单完成推送BS → LIMS
def report_order_finish(self,
order_code: str,
order_name: str,
start_time: str,
end_time: str,
status: str = "30", # 30 完成 / -11 异常停止 / -12 人工停止
workflow_status: str = "Finished",
completion_time: Optional[str] = None,
used_materials: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]:
data = {
"orderCode": order_code,
"orderName": order_name,
"startTime": start_time,
"endTime": end_time,
"status": status,
"workflowStatus": workflow_status,
"completionTime": completion_time or end_time,
"usedMaterials": used_materials or []
}
return self._post_report("/report/order_finish", data)
# 2.5 批量查询实验报告(用于轮询是否完成)
def order_list(self,
status: Optional[str] = None,
begin_time: Optional[str] = None,
end_time: Optional[str] = None,
filter_text: Optional[str] = None,
skip: int = 0, page: int = 10) -> Dict[str, Any]:
data: Dict[str, Any] = {"skipCount": skip, "pageCount": page}
if status is not None: # 80 成功 / 90 失败 / 100 执行中
data["status"] = status
if begin_time:
data["timeType"] = "CreationTime"
data["beginTime"] = begin_time
if end_time:
data["endTime"] = end_time
if filter_text:
data["filter"] = filter_text
return self._post_lims("/api/lims/order/order-list", data)
# 2.6 实验报告查询根据任务ID拿详情
def order_report(self, order_id: str) -> Dict[str, Any]:
return self._post_lims("/api/lims/order/order-report", order_id)
# 2.32 3-2-1 物料转运
def transfer_3_to_2_to_1(self,
# source_wh_id: Optional[str] = None,
source_wh_id: Optional[str] = '3a19debc-84b4-0359-e2d4-b3beea49348b',
source_x: int = 1, source_y: int = 1, source_z: int = 1) -> Dict[str, Any]:
payload: Dict[str, Any] = {
"sourcePosX": source_x, "sourcePosY": source_y, "sourcePosZ": source_z
}
if source_wh_id:
payload["sourceWHID"] = source_wh_id
return self._post_lims("/api/lims/order/transfer-task3To2To1", payload)
# 2.28 样品/废料取出
def take_out(self,
order_id: str,
preintake_ids: Optional[List[str]] = None,
material_ids: Optional[List[str]] = None) -> Dict[str, Any]:
data = {
"orderId": order_id,
"preintakeIds": preintake_ids or [],
"materialIds": material_ids or []
}
return self._post_lims("/api/lims/order/take-out", data)
# --------可选占位方法文档未定义的“1号站内部流程 / 1-2转运”--------
def start_station1_internal_flow(self, **kwargs) -> None:
logger.info("启动1号站内部流程占位按现场系统填充具体指令")
# 3.x 1→2 物料转运
def transfer_1_to_2(self) -> Dict[str, Any]:
"""
1→2 物料转运
URL: /api/lims/order/transfer-task1To2
只需要 apiKey 和 requestTime
"""
return self._post_lims("/api/lims/order/transfer-task1To2")
# -------------------- 整体编排 --------------------
def run_full_workflow(self,
inbound_items: List[Dict[str, str]],
orders: List[Dict[str, Any]],
poll_filter_code: Optional[str] = None,
poll_timeout_s: int = 600,
poll_interval_s: int = 5,
transfer_source: Optional[Dict[str, Any]] = None,
takeout_order_id: Optional[str] = None) -> None:
"""
一键串联:
1) 入库 3-4 个物料 → 2) 新建实验 → 3) 启动调度
运行中如需4) 物料变更推送 5) 步骤完成推送 6) 订单完成推送
完成后查询实验2.5/2.6)→ 7) 3-2-1 转运 → 8) 1号站内部流程
→ 9) 1-2 转运 → 10) 样品/废料取出
"""
# 1. 入库多于1个就用批量接口 2.18
if len(inbound_items) == 1:
r = self.storage_inbound(inbound_items[0]["materialId"], inbound_items[0]["locationId"])
logger.info(f"单个入库结果: {r}")
else:
r = self.storage_batch_inbound(inbound_items)
logger.info(f"批量入库结果: {r}")
# 2. 新建实验2.14
r = self.create_orders(orders)
logger.info(f"新建实验结果: {r}")
# 3. 启动调度2.7
r = self.scheduler_start()
logger.info(f"启动调度结果: {r}")
# —— 运行中各类推送2.24 / 2.21 / 2.23),通常由实际任务驱动,这里提供调用方式 —— #
# self.report_material_change({...})
# self.report_step_finish(order_code="BSO...", order_name="配液分液", step_name="xxx", step_id="...", sample_id="...",
# start_time=_iso_utc_now_ms(), end_time=_iso_utc_now_ms(), execution_status="completed")
# self.report_order_finish(order_code="BSO...", order_name="配液分液", start_time="...", end_time=_iso_utc_now_ms())
# 完成后才能转运:用 2.5 批量查询配合 filter=任务编码 轮询到 status=80成功
if poll_filter_code:
import time
deadline = time.time() + poll_timeout_s
while time.time() < deadline:
res = self.order_list(status="80", filter_text=poll_filter_code, page=5)
if isinstance(res, dict) and res.get("data", {}).get("items"):
logger.info(f"实验 {poll_filter_code} 已完成:{res['data']['items'][0]}")
break
time.sleep(poll_interval_s)
else:
logger.warning(f"等待实验 {poll_filter_code} 完成超时(未到 status=80")
# 7. 启动 3-2-1 转运2.32
if transfer_source:
r = self.transfer_3_to_2_to_1(
source_wh_id=transfer_source.get("sourceWHID"),
source_x=transfer_source.get("sourcePosX", 1),
source_y=transfer_source.get("sourcePosY", 1),
source_z=transfer_source.get("sourcePosZ", 1),
)
logger.info(f"3-2-1 转运结果: {r}")
# 8. 1号站内部流程占位
self.start_station1_internal_flow()
# 9. 1→2 转运(占位)
self.transfer_1_to_2()
# 10. 样品/废料取出2.28
if takeout_order_id:
r = self.take_out(order_id=takeout_order_id)
logger.info(f"样品/废料取出结果: {r}")
# 套接字服务端收到“步骤完成”时调用
def process_step_finish_report(self, report_request):
order_code = report_request.data.get("orderCode")
if order_code:
self.order_status[order_code] = "step_finished"
logger.info(f"[REPORT] 订单 {order_code} 步骤完成")
return {"ack": True}
def process_order_finish_report(self, report_request, used_materials=None):
order_code = report_request.data.get("orderCode")
if order_code:
self.order_status[order_code] = "finished"
logger.info(f"[REPORT] 订单 {order_code} 已完成,状态改为 finished")
return {"ack": True, "usedMaterials": used_materials or []}
# 收到“通量完成”时调用
def process_sample_finish_report(self, report_request):
order_code = report_request.data.get("orderCode")
if order_code:
self.order_status[order_code] = "sample_finished"
logger.info(f"[REPORT] 订单 {order_code} 通量完成")
return {"ack": True}
def test_benyao_workstation(self, num1, num2):
num1 = int(num1)
num2 = int(num2)
for i in range(num1):
print(f"num1 = {num1}")
for j in range(num2):
print(f"num1 = {num2}")
test_benyao_api()
# --------------------------------
if __name__ == "__main__":
ws = BioyondWorkstation()
# ws.scheduler_stop()
# 3.30 上料:读取模板 Excel 自动解析并 POST
# r1 = ws.auto_feeding4to3_from_xlsx(r"C:\ML\GitHub\Uni-Lab-OS\unilabos\devices\workstation\bioyond_cell\样品导入模板 (8).xlsx")
# ws.scheduler_start()
# print(r1["payload"]["data"]) # 调试模式下可直接看到要发的 JSON items
# 新建实验
# res = ws.create_orders("C:/ML/GitHub/Uni-Lab-OS/unilabos/devices/workstation/bioyond_cell/2025092501.xlsx")
# print(res)
# 3.31 下料:同理
# r2 = ws.auto_batch_outbound_from_xlsx(r"C:/path/样品导入模板 (8).xlsx")
# print(r2["payload"]["data"])
# r321 = ws.transfer_3_to_2_to_1()
# ws.transfer_1_to_2()

View File

@@ -0,0 +1,706 @@
{
"nodes": [
{
"id": "bioyond_workstation",
"name": "配液分液工站",
"children": [
],
"parent": null,
"type": "device",
"class": "bioyondworkstation_device",
"config": {
"protocol_type": [],
"station_resource": {}
},
"data": {}
},
{
"id": "BatteryStation",
"name": "扣电工作站",
"children": [
"coin_cell_deck"
],
"parent": null,
"type": "device",
"class": "bettery_station_registry",
"position": {
"x": 600,
"y": 400,
"z": 0
},
"config": {
"debug_mode": false,
"_comment": "protocol_type接外部工站固定写法字段一般为空station_resource写法也固定",
"protocol_type": [],
"station_resource": {
"data": {
"_resource_child_name": "coin_cell_deck",
"_resource_type": "unilabos.devices.workstation.coin_cell_assembly.button_battery_station:CoincellDeck"
}
},
"address": "192.168.1.20",
"port": 502
},
"data": {}
},
{
"id": "coin_cell_deck",
"name": "coin_cell_deck",
"sample_id": null,
"children": [
"\u7535\u6c60\u6599\u76d8"
],
"parent": null,
"type": "container",
"class": "",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"type": "CoincellDeck",
"size_x": 1000,
"size_y": 1000,
"size_z": 900,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "coin_cell_deck",
"barcode": null
},
"data": {}
},
{
"id": "\u7535\u6c60\u6599\u76d8",
"name": "\u7535\u6c60\u6599\u76d8",
"sample_id": null,
"children": [
"\u7535\u6c60\u6599\u76d8_materialhole_0_0",
"\u7535\u6c60\u6599\u76d8_materialhole_0_1",
"\u7535\u6c60\u6599\u76d8_materialhole_0_2",
"\u7535\u6c60\u6599\u76d8_materialhole_0_3",
"\u7535\u6c60\u6599\u76d8_materialhole_1_0",
"\u7535\u6c60\u6599\u76d8_materialhole_1_1",
"\u7535\u6c60\u6599\u76d8_materialhole_1_2",
"\u7535\u6c60\u6599\u76d8_materialhole_1_3",
"\u7535\u6c60\u6599\u76d8_materialhole_2_0",
"\u7535\u6c60\u6599\u76d8_materialhole_2_1",
"\u7535\u6c60\u6599\u76d8_materialhole_2_2",
"\u7535\u6c60\u6599\u76d8_materialhole_2_3",
"\u7535\u6c60\u6599\u76d8_materialhole_3_0",
"\u7535\u6c60\u6599\u76d8_materialhole_3_1",
"\u7535\u6c60\u6599\u76d8_materialhole_3_2",
"\u7535\u6c60\u6599\u76d8_materialhole_3_3"
],
"parent": "coin_cell_deck",
"type": "container",
"class": "",
"position": {
"x": 100,
"y": 100,
"z": 0
},
"config": {
"type": "MaterialPlate",
"size_x": 120.8,
"size_y": 160.5,
"size_z": 10.0,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_plate",
"model": null,
"barcode": null,
"ordering": {
"A1": "\u7535\u6c60\u6599\u76d8_materialhole_0_0",
"B1": "\u7535\u6c60\u6599\u76d8_materialhole_0_1",
"C1": "\u7535\u6c60\u6599\u76d8_materialhole_0_2",
"D1": "\u7535\u6c60\u6599\u76d8_materialhole_0_3",
"A2": "\u7535\u6c60\u6599\u76d8_materialhole_1_0",
"B2": "\u7535\u6c60\u6599\u76d8_materialhole_1_1",
"C2": "\u7535\u6c60\u6599\u76d8_materialhole_1_2",
"D2": "\u7535\u6c60\u6599\u76d8_materialhole_1_3",
"A3": "\u7535\u6c60\u6599\u76d8_materialhole_2_0",
"B3": "\u7535\u6c60\u6599\u76d8_materialhole_2_1",
"C3": "\u7535\u6c60\u6599\u76d8_materialhole_2_2",
"D3": "\u7535\u6c60\u6599\u76d8_materialhole_2_3",
"A4": "\u7535\u6c60\u6599\u76d8_materialhole_3_0",
"B4": "\u7535\u6c60\u6599\u76d8_materialhole_3_1",
"C4": "\u7535\u6c60\u6599\u76d8_materialhole_3_2",
"D4": "\u7535\u6c60\u6599\u76d8_materialhole_3_3"
}
},
"data": {}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_0_0",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_0_0",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 12.4,
"y": 104.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_0_1",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_0_1",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 12.4,
"y": 80.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_0_2",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_0_2",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 12.4,
"y": 56.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_0_3",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_0_3",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 12.4,
"y": 32.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_1_0",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_1_0",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 36.4,
"y": 104.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_1_1",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_1_1",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 36.4,
"y": 80.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_1_2",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_1_2",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 36.4,
"y": 56.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_1_3",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_1_3",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 36.4,
"y": 32.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_2_0",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_2_0",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 60.4,
"y": 104.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_2_1",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_2_1",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 60.4,
"y": 80.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_2_2",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_2_2",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 60.4,
"y": 56.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_2_3",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_2_3",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 60.4,
"y": 32.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_3_0",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_3_0",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 84.4,
"y": 104.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_3_1",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_3_1",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 84.4,
"y": 80.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_3_2",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_3_2",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 84.4,
"y": 56.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_3_3",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_3_3",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 84.4,
"y": 32.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
}
],
"links": []
}

View File

@@ -0,0 +1,20 @@
{
"nodes": [
{
"id": "bioyond_workstation",
"name": "配液分液工站",
"children": [
],
"parent": null,
"type": "device",
"class": "bioyondworkstation_device",
"config": {
"protocol_type": [],
"station_resource": {}
},
"data": {}
}
],
"links": []
}

View File

@@ -0,0 +1,374 @@
"""
Bioyond物料管理实现
Bioyond Material Management Implementation
基于Bioyond系统的物料管理支持从Bioyond系统同步物料到UniLab工作站
"""
from typing import Dict, Any, List, Optional, Union
import json
import asyncio
from abc import ABC, abstractmethod
from pylabrobot.resources import (
Resource as PLRResource,
Container,
Deck,
Coordinate as PLRCoordinate,
)
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
from unilabos.utils.log import logger
from unilabos.resources.graphio import (
resource_plr_to_ulab,
resource_ulab_to_plr,
resource_bioyond_to_ulab,
resource_bioyond_container_to_ulab,
resource_ulab_to_bioyond
)
from .workstation_material_management import MaterialManagementBase
class BioyondMaterialManagement(MaterialManagementBase):
"""Bioyond物料管理类
实现从Bioyond系统同步物料到UniLab工作站的功能
1. 从Bioyond系统获取物料数据
2. 转换为UniLab格式
3. 同步到PyLabRobot Deck
4. 支持双向同步
"""
def __init__(
self,
device_id: str,
deck_config: Dict[str, Any],
resource_tracker: DeviceNodeResourceTracker,
children_config: Dict[str, Dict[str, Any]] = None,
bioyond_config: Dict[str, Any] = None
):
self.bioyond_config = bioyond_config or {}
self.bioyond_api_client = None
self.sync_interval = self.bioyond_config.get("sync_interval", 30) # 同步间隔(秒)
# 初始化父类
super().__init__(device_id, deck_config, resource_tracker, children_config)
# 初始化Bioyond API客户端
self._initialize_bioyond_client()
# 启动同步任务
self._start_sync_task()
def _initialize_bioyond_client(self):
"""初始化Bioyond API客户端"""
try:
# 这里应该根据实际的Bioyond API实现
# 暂时使用模拟客户端
self.bioyond_api_client = BioyondAPIClient(self.bioyond_config)
logger.info(f"Bioyond API客户端初始化成功")
except Exception as e:
logger.error(f"Bioyond API客户端初始化失败: {e}")
self.bioyond_api_client = None
def _start_sync_task(self):
"""启动同步任务"""
if self.bioyond_api_client:
# 创建异步同步任务
asyncio.create_task(self._periodic_sync())
logger.info(f"Bioyond同步任务已启动间隔: {self.sync_interval}")
async def _periodic_sync(self):
"""定期同步任务"""
while True:
try:
await self.sync_from_bioyond()
await asyncio.sleep(self.sync_interval)
except Exception as e:
logger.error(f"Bioyond同步任务出错: {e}")
await asyncio.sleep(self.sync_interval)
async def sync_from_bioyond(self) -> bool:
"""从Bioyond系统同步物料"""
try:
if not self.bioyond_api_client:
logger.warning("Bioyond API客户端未初始化")
return False
# 1. 从Bioyond获取物料数据
bioyond_data = await self.bioyond_api_client.get_materials()
if not bioyond_data:
logger.warning("从Bioyond获取物料数据为空")
return False
# 2. 转换为UniLab格式
if isinstance(bioyond_data, dict) and "data" in bioyond_data:
# 容器格式数据
unilab_resources = resource_bioyond_container_to_ulab(bioyond_data)
else:
# 物料列表格式数据
unilab_resources = resource_bioyond_to_ulab(bioyond_data)
# 3. 转换为PLR格式并分配到Deck
await self._assign_resources_to_deck(unilab_resources)
logger.info(f"从Bioyond同步了 {len(unilab_resources)} 个资源")
return True
except Exception as e:
logger.error(f"从Bioyond同步物料失败: {e}")
return False
async def sync_to_bioyond(self, plr_resource: PLRResource) -> bool:
"""将本地物料变更同步到Bioyond系统"""
try:
if not self.bioyond_api_client:
logger.warning("Bioyond API客户端未初始化")
return False
# 1. 转换为UniLab格式
unilab_resource = resource_plr_to_ulab(plr_resource)
# 2. 转换为Bioyond格式
bioyond_materials = resource_ulab_to_bioyond([unilab_resource])
# 3. 发送到Bioyond系统
success = await self.bioyond_api_client.update_materials(bioyond_materials)
if success:
logger.info(f"成功同步物料 {plr_resource.name} 到Bioyond")
else:
logger.warning(f"同步物料 {plr_resource.name} 到Bioyond失败")
return success
except Exception as e:
logger.error(f"同步物料到Bioyond失败: {e}")
return False
async def _assign_resources_to_deck(self, unilab_resources: List[Dict[str, Any]]):
"""将UniLab资源分配到Deck"""
try:
# 转换为PLR格式
from unilabos.resources.graphio import list_to_nested_dict
nested_resources = list_to_nested_dict(unilab_resources)
plr_resources = resource_ulab_to_plr(nested_resources)
# 分配资源到Deck
if hasattr(plr_resources, 'children'):
resources_to_assign = plr_resources.children
elif isinstance(plr_resources, list):
resources_to_assign = plr_resources
else:
resources_to_assign = [plr_resources]
for resource in resources_to_assign:
try:
# 获取资源位置
if hasattr(resource, 'location') and resource.location:
location = PLRCoordinate(resource.location.x, resource.location.y, resource.location.z)
else:
location = PLRCoordinate(0, 0, 0)
# 分配资源到Deck
self.plr_deck.assign_child_resource(resource, location)
# 注册到resource tracker
self.resource_tracker.add_resource(resource)
# 保存资源引用
self.plr_resources[resource.name] = resource
except Exception as e:
logger.error(f"分配资源 {resource.name} 到Deck失败: {e}")
logger.info(f"成功分配了 {len(resources_to_assign)} 个资源到Deck")
except Exception as e:
logger.error(f"分配资源到Deck失败: {e}")
def _create_resource_by_type(
self,
resource_id: str,
resource_type: str,
config: Dict[str, Any],
data: Dict[str, Any],
location: PLRCoordinate
) -> Optional[PLRResource]:
"""根据类型创建Bioyond相关资源"""
try:
# 这里可以根据需要实现特定的Bioyond资源类型
# 目前使用通用的容器类型
if resource_type in ["container", "plate", "well"]:
return self._create_generic_container(resource_id, resource_type, config, data, location)
else:
logger.warning(f"未知的Bioyond资源类型: {resource_type}")
return None
except Exception as e:
logger.error(f"创建Bioyond资源失败 {resource_id} ({resource_type}): {e}")
return None
def _create_generic_container(
self,
resource_id: str,
resource_type: str,
config: Dict[str, Any],
data: Dict[str, Any],
location: PLRCoordinate
) -> Optional[PLRResource]:
"""创建通用容器资源"""
try:
from pylabrobot.resources import Plate, Well
if resource_type == "plate":
return Plate(
name=resource_id,
size_x=config.get("size_x", 127.76),
size_y=config.get("size_y", 85.48),
size_z=config.get("size_z", 14.35),
location=location,
category="plate"
)
elif resource_type == "well":
return Well(
name=resource_id,
size_x=config.get("size_x", 9.0),
size_y=config.get("size_y", 9.0),
size_z=config.get("size_z", 10.0),
location=location,
category="well"
)
else:
return Container(
name=resource_id,
size_x=config.get("size_x", 50.0),
size_y=config.get("size_y", 50.0),
size_z=config.get("size_z", 10.0),
location=location,
category="container"
)
except Exception as e:
logger.error(f"创建通用容器失败 {resource_id}: {e}")
return None
def get_bioyond_materials(self) -> List[Dict[str, Any]]:
"""获取当前Bioyond物料列表"""
try:
# 将当前PLR资源转换为Bioyond格式
bioyond_materials = []
for resource in self.plr_resources.values():
unilab_resource = resource_plr_to_ulab(resource)
bioyond_materials.extend(resource_ulab_to_bioyond([unilab_resource]))
return bioyond_materials
except Exception as e:
logger.error(f"获取Bioyond物料列表失败: {e}")
return []
def update_material_from_bioyond(self, material_id: str, bioyond_data: Dict[str, Any]) -> bool:
"""从Bioyond数据更新指定物料"""
try:
# 查找现有物料
material = self.find_material_by_id(material_id)
if not material:
logger.warning(f"未找到物料: {material_id}")
return False
# 转换Bioyond数据为UniLab格式
unilab_resources = resource_bioyond_to_ulab([bioyond_data])
if not unilab_resources:
logger.warning(f"转换Bioyond数据失败: {material_id}")
return False
# 更新物料属性
unilab_resource = unilab_resources[0]
material.name = unilab_resource.get("name", material.name)
# 更新位置
position = unilab_resource.get("position", {})
if position:
material.location = PLRCoordinate(
position.get("x", 0),
position.get("y", 0),
position.get("z", 0)
)
logger.info(f"成功更新物料: {material_id}")
return True
except Exception as e:
logger.error(f"更新物料失败 {material_id}: {e}")
return False
class BioyondAPIClient:
"""Bioyond API客户端模拟实现
实际使用时需要根据Bioyond系统的API接口实现
"""
def __init__(self, config: Dict[str, Any]):
self.config = config
self.base_url = config.get("base_url", "http://localhost:8080")
self.api_key = config.get("api_key", "")
self.timeout = config.get("timeout", 30)
async def get_materials(self) -> Optional[Union[Dict[str, Any], List[Dict[str, Any]]]]:
"""从Bioyond系统获取物料数据"""
try:
# 这里应该实现实际的API调用
# 暂时返回模拟数据
logger.info("从Bioyond API获取物料数据")
# 模拟API调用延迟
await asyncio.sleep(0.1)
# 返回模拟数据实际应该从API获取
return {
"data": [],
"code": 1,
"message": "success",
"timestamp": 1234567890
}
except Exception as e:
logger.error(f"Bioyond API调用失败: {e}")
return None
async def update_materials(self, materials: List[Dict[str, Any]]) -> bool:
"""更新Bioyond系统中的物料数据"""
try:
# 这里应该实现实际的API调用
logger.info(f"更新Bioyond系统中的 {len(materials)} 个物料")
# 模拟API调用延迟
await asyncio.sleep(0.1)
# 模拟成功响应
return True
except Exception as e:
logger.error(f"更新Bioyond物料失败: {e}")
return False
async def get_material_by_id(self, material_id: str) -> Optional[Dict[str, Any]]:
"""根据ID获取单个物料"""
try:
# 这里应该实现实际的API调用
logger.info(f"从Bioyond API获取物料: {material_id}")
# 模拟API调用延迟
await asyncio.sleep(0.1)
# 返回模拟数据
return {
"id": material_id,
"name": f"material_{material_id}",
"type": "container",
"quantity": 1.0,
"unit": ""
}
except Exception as e:
logger.error(f"获取Bioyond物料失败 {material_id}: {e}")
return None

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -24,6 +24,13 @@ class ElectrodeSheetState(TypedDict):
thickness: float # 厚度 (mm)
mass: float # 质量 (g)
material_type: str # 材料类型(正极、负极、隔膜、弹片、垫片、铝箔等)
height: float
electrolyte_name: str
data_electrolyte_code: str
open_circuit_voltage: float
assembly_pressure: float
electrolyte_volume: float
info: Optional[str] # 附加信息
class ElectrodeSheet(Resource):
@@ -147,10 +154,10 @@ class MaterialHole(Resource):
):
"""放置极片"""
# TODO: 这里要改diameter找不到加入._unilabos_state后应该没问题
if resource._unilabos_state["diameter"] > self._unilabos_state["diameter"]:
raise ValueError(f"极片直径 {resource._unilabos_state['diameter']} 超过洞位直径 {self._unilabos_state['diameter']}")
if len(self.children) >= self._unilabos_state["max_sheets"]:
raise ValueError(f"洞位已满,无法放置更多极片")
#if resource._unilabos_state["diameter"] > self._unilabos_state["diameter"]:
# raise ValueError(f"极片直径 {resource._unilabos_state['diameter']} 超过洞位直径 {self._unilabos_state['diameter']}")
#if len(self.children) >= self._unilabos_state["max_sheets"]:
# raise ValueError(f"洞位已满,无法放置更多极片")
super().assign_child_resource(resource, location, reassign)
# 根据children的编号取物料对象。
@@ -165,8 +172,6 @@ class MaterialPlateState(TypedDict):
hole_diameter: float
info: Optional[str] # 附加信息
class MaterialPlate(ItemizedResource[MaterialHole]):
"""料板类 - 4x4个洞位每个洞位放1个极片"""
@@ -324,12 +329,13 @@ class PlateSlot(ResourceStack):
class ClipMagazineHole(Container):
"""子弹夹洞位类"""
children: List[ElectrodeSheet] = []
def __init__(
self,
name: str,
diameter: float,
depth: float,
max_sheets: int = 100,
category: str = "clip_magazine_hole",
):
"""初始化子弹夹洞位
@@ -338,6 +344,7 @@ class ClipMagazineHole(Container):
name: 洞位名称
diameter: 洞直径 (mm)
depth: 洞深度 (mm)
max_sheets: 最大极片数量
category: 类别
"""
super().__init__(
@@ -349,143 +356,46 @@ class ClipMagazineHole(Container):
)
self.diameter = diameter
self.depth = depth
self.max_sheets = max_sheets
self._sheets: List[ElectrodeSheet] = []
def can_add_sheet(self, sheet: ElectrodeSheet) -> bool:
"""检查是否可以添加极片
根据洞的深度和极片的厚度来判断是否可以添加极片
"""
# 检查极片直径是否适合洞的直径
if sheet._unilabos_state["diameter"] > self.diameter:
return False
# 计算当前已添加极片的总厚度
current_thickness = sum(s._unilabos_state["thickness"] for s in self.children)
# 检查添加新极片后总厚度是否超过洞的深度
if current_thickness + sheet._unilabos_state["thickness"] > self.depth:
return False
return True
"""检查是否可以添加极片"""
return (len(self._sheets) < self.max_sheets and
sheet.diameter <= self.diameter)
def add_sheet(self, sheet: ElectrodeSheet) -> None:
"""添加极片"""
if not self.can_add_sheet(sheet):
raise ValueError(f"无法向洞位 {self.name} 添加极片")
self._sheets.append(sheet)
def assign_child_resource(
self,
resource: ElectrodeSheet,
location: Optional[Coordinate] = None,
reassign: bool = True,
):
"""放置极片到洞位中
Args:
resource: 要放置的极片
location: 极片在洞位中的位置对于洞位通常为None
reassign: 是否允许重新分配
"""
# 检查是否可以添加极片
if not self.can_add_sheet(resource):
raise ValueError(f"无法向洞位 {self.name} 添加极片:直径或厚度不匹配")
# 调用父类方法实际执行分配
super().assign_child_resource(resource, location, reassign)
def unassign_child_resource(self, resource: ElectrodeSheet):
"""从洞位中移除极片
Args:
resource: 要移除的极片
"""
if resource not in self.children:
raise ValueError(f"极片 {resource.name} 不在洞位 {self.name}")
# 调用父类方法实际执行移除
super().unassign_child_resource(resource)
def take_sheet(self) -> ElectrodeSheet:
"""取出极片"""
if len(self._sheets) == 0:
raise ValueError(f"洞位 {self.name} 没有极片")
return self._sheets.pop()
def get_sheet_count(self) -> int:
"""获取极片数量"""
return len(self._sheets)
def serialize_state(self) -> Dict[str, Any]:
return {
"sheet_count": len(self.children),
"sheets": [sheet.serialize() for sheet in self.children],
"sheet_count": len(self._sheets),
"sheets": [sheet.serialize() for sheet in self._sheets],
}
class ClipMagazine_four(ItemizedResource[ClipMagazineHole]):
"""子弹夹类 - 有4个洞位每个洞位放多个极片"""
children: List[ClipMagazineHole]
def __init__(
self,
name: str,
size_x: float,
size_y: float,
size_z: float,
hole_diameter: float = 14.0,
hole_depth: float = 10.0,
hole_spacing: float = 25.0,
max_sheets_per_hole: int = 100,
category: str = "clip_magazine_four",
model: Optional[str] = None,
):
"""初始化子弹夹
Args:
name: 子弹夹名称
size_x: 长度 (mm)
size_y: 宽度 (mm)
size_z: 高度 (mm)
hole_diameter: 洞直径 (mm)
hole_depth: 洞深度 (mm)
hole_spacing: 洞位间距 (mm)
max_sheets_per_hole: 每个洞位最大极片数量
category: 类别
model: 型号
"""
# 创建4个洞位排成2x2布局
holes = create_ordered_items_2d(
klass=ClipMagazineHole,
num_items_x=2,
num_items_y=2,
dx=(size_x - 2 * hole_spacing) / 2, # 居中
dy=(size_y - hole_spacing) / 2, # 居中
dz=size_z - 0,
item_dx=hole_spacing,
item_dy=hole_spacing,
diameter=hole_diameter,
depth=hole_depth,
)
super().__init__(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
ordered_items=holes,
category=category,
model=model,
)
# 保存洞位的直径和深度
self.hole_diameter = hole_diameter
self.hole_depth = hole_depth
self.max_sheets_per_hole = max_sheets_per_hole
def serialize(self) -> dict:
return {
**super().serialize(),
"hole_diameter": self.hole_diameter,
"hole_depth": self.hole_depth,
"max_sheets_per_hole": self.max_sheets_per_hole,
}
# TODO: 这个要改
class ClipMagazine(ItemizedResource[ClipMagazineHole]):
class ClipMagazine(Resource):
"""子弹夹类 - 有6个洞位每个洞位放多个极片"""
children: List[ClipMagazineHole]
def __init__(
self,
name: str,
size_x: float,
size_y: float,
size_z: float,
hole_diameter: float = 14.0,
hole_depth: float = 10.0,
hole_spacing: float = 25.0,
max_sheets_per_hole: int = 100,
category: str = "clip_magazine",
@@ -515,8 +425,8 @@ class ClipMagazine(ItemizedResource[ClipMagazineHole]):
dz=size_z - 0,
item_dx=hole_spacing,
item_dy=hole_spacing,
diameter=hole_diameter,
depth=hole_depth,
diameter=0,
depth=0,
)
super().__init__(
@@ -529,7 +439,6 @@ class ClipMagazine(ItemizedResource[ClipMagazineHole]):
model=model,
)
# 保存洞位的直径和深度
self.hole_diameter = hole_diameter
self.hole_depth = hole_depth
self.max_sheets_per_hole = max_sheets_per_hole
@@ -546,9 +455,9 @@ class BatteryState(TypedDict):
"""电池状态字典"""
diameter: float
height: float
electrolyte_name: str
assembly_pressure: float
electrolyte_volume: float
electrolyte_name: str
class Battery(Resource):
"""电池类 - 可容纳极片"""
@@ -557,6 +466,9 @@ class Battery(Resource):
def __init__(
self,
name: str,
size_x=1,
size_y=1,
size_z=1,
category: str = "battery",
):
"""初始化电池
@@ -577,7 +489,13 @@ class Battery(Resource):
size_z=1,
category=category,
)
self._unilabos_state: BatteryState = BatteryState()
self._unilabos_state: BatteryState = BatteryState(
diameter = 1.0,
height = 1.0,
assembly_pressure = 1.0,
electrolyte_volume = 1.0,
electrolyte_name = "DP001"
)
def add_electrolyte_with_bottle(self, bottle: Bottle) -> bool:
to_add_name = bottle._unilabos_state["electrolyte_name"]
@@ -733,15 +651,6 @@ class TipBox64(TipRack):
make_tip=make_tip,
)
self._unilabos_state: WasteTipBoxstate = WasteTipBoxstate()
# 记录网格参数用于前端渲染
self._grid_params = {
"num_items_x": 8,
"num_items_y": 8,
"dx": 8.0,
"dy": 8.0,
"item_dx": 9.0,
"item_dy": 9.0,
}
super().__init__(
name=name,
size_x=size_x,
@@ -753,12 +662,6 @@ class TipBox64(TipRack):
with_tips=True,
)
def serialize(self) -> dict:
return {
**super().serialize(),
**self._grid_params,
}
class WasteTipBoxstate(TypedDict):
@@ -836,34 +739,19 @@ class BottleRackState(TypedDict):
name_to_index: dict
class BottleRackState(TypedDict):
""" bottle_diameter: 瓶子直径 (mm)
bottle_height: 瓶子高度 (mm)
position_spacing: 位置间距 (mm)"""
bottle_diameter: float
bottle_height: float
position_spacing: float
name_to_index: dict
class BottleRack(Resource):
"""瓶架类 - 12个待配位置+12个已配位置"""
children: List[Resource] = []
children: List[Bottle] = []
def __init__(
self,
name: str,
size_x: float,
size_y: float,
size_z: float,
category: str = "bottle_rack",
model: Optional[str] = None,
num_items_x: int = 3,
num_items_y: int = 4,
position_spacing: float = 35.0,
orientation: str = "horizontal",
padding_x: float = 20.0,
padding_y: float = 20.0,
self,
name: str,
size_x: float,
size_y: float,
size_z: float,
category: str = "bottle_rack",
model: Optional[str] = None,
):
"""初始化瓶架
@@ -883,42 +771,13 @@ class BottleRack(Resource):
category=category,
model=model,
)
# 初始化状态
self._unilabos_state: BottleRackState = BottleRackState(
bottle_diameter=30.0,
bottle_height=100.0,
position_spacing=position_spacing,
name_to_index={},
)
# 基于网格生成瓶位坐标映射(居中摆放)
# 使用内边距避免点跑到容器外前端渲染不按mm等比缩放时更稳妥
origin_x = padding_x
origin_y = padding_y
self.index_to_pos = {}
for j in range(num_items_y):
for i in range(num_items_x):
idx = j * num_items_x + i
if orientation == "vertical":
# 纵向:沿 y 方向优先排列
self.index_to_pos[idx] = Coordinate(
x=origin_x + j * position_spacing,
y=origin_y + i * position_spacing,
z=0,
)
else:
# 横向(默认):沿 x 方向优先排列
self.index_to_pos[idx] = Coordinate(
x=origin_x + i * position_spacing,
y=origin_y + j * position_spacing,
z=0,
)
# TODO: 添加瓶位坐标映射
self.index_to_pos = {
0: Coordinate.zero(),
1: Coordinate(x=1, y=2, z=3) # 添加
}
self.name_to_index = {}
self.name_to_pos = {}
self.num_items_x = num_items_x
self.num_items_y = num_items_y
self.orientation = orientation
self.padding_x = padding_x
self.padding_y = padding_y
def load_state(self, state: Dict[str, Any]) -> None:
"""格式不变"""
@@ -928,23 +787,20 @@ class BottleRack(Resource):
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
"""格式不变"""
data = super().serialize_state()
data.update(
self._unilabos_state) # Container自身的信息云端物料将保存这一data本地也通过这里的data进行读写当前类用来表示这个物料的长宽高大小的属性而datastate用来表示物料的内容细节等
data.update(self._unilabos_state) # Container自身的信息云端物料将保存这一data本地也通过这里的data进行读写当前类用来表示这个物料的长宽高大小的属性而datastate用来表示物料的内容细节等
return data
# TODO: 这里有些问题要重新写一下
def assign_child_resource_old(self, resource: Resource, location=Coordinate.zero(), reassign=True):
capacity = self.num_items_x * self.num_items_y
assert len(self.children) < capacity, "瓶架已满,无法添加更多瓶子"
def assign_child_resource(self, resource: Bottle, location=Coordinate.zero(), reassign = True):
assert len(self.children) <= 12, "瓶架已满,无法添加更多瓶子"
index = len(self.children)
location = self.index_to_pos.get(index, Coordinate.zero())
location = Coordinate(x=20 + (index % 4) * 15, y=20 + (index // 4) * 15, z=0)
self.name_to_pos[resource.name] = location
self.name_to_index[resource.name] = index
return super().assign_child_resource(resource, location, reassign)
def assign_child_resource(self, resource: Resource, index: int):
capacity = self.num_items_x * self.num_items_y
assert 0 <= index < capacity, "无效的瓶子索引"
def assign_child_resource_by_index(self, resource: Bottle, index: int):
assert 0 <= index < 12, "无效的瓶子索引"
self.name_to_index[resource.name] = index
location = self.index_to_pos[index]
return super().assign_child_resource(resource, location)
@@ -953,16 +809,9 @@ class BottleRack(Resource):
super().unassign_child_resource(resource)
self.index_to_pos.pop(self.name_to_index.pop(resource.name, None), None)
def serialize(self) -> dict:
return {
**super().serialize(),
"num_items_x": self.num_items_x,
"num_items_y": self.num_items_y,
"position_spacing": self._unilabos_state.get("position_spacing", 35.0),
"orientation": self.orientation,
"padding_x": self.padding_x,
"padding_y": self.padding_y,
}
# def serialize(self):
# self.children.sort(key=lambda x: self.name_to_index.get(x.name, 0))
# return super().serialize()
class BottleState(TypedDict):
@@ -1098,16 +947,20 @@ class CoincellDeck(Deck):
# plate.assign_child_resource(hole)
# return plate
def create_a_liaopan():
liaopan = MaterialPlate(name="liaopan", size_x=120.8, size_y=120.5, size_z=10.0, fill=True)
for i in range(16):
jipian = ElectrodeSheet(name=f"jipian_{i}", size_x= 12, size_y=12, size_z=0.1)
liaopan1.children[i].assign_child_resource(jipian, location=None)
return liaopan
import json
def create_a_coin_cell_deck():
deck = Deck(size_x=1200,
size_y=800,
if __name__ == "__main__":
#electrode1 = BatteryPressSlot()
#print(electrode1.get_size_x())
#print(electrode1.get_size_y())
#print(electrode1.get_size_z())
#jipian = ElectrodeSheet()
#jipian._unilabos_state["diameter"] = 18
#print(jipian.serialize())
#print(jipian.serialize_state())
deck = CoincellDeck(size_x=1000,
size_y=1000,
size_z=900)
#liaopan = TipBox64(name="liaopan")
@@ -1118,172 +971,33 @@ def create_a_coin_cell_deck():
deck.assign_child_resource(liaopan1, Coordinate(x=0, y=0, z=0))
#创建一个极片
for i in range(16):
jipian = ElectrodeSheet(name=f"jipian_{i}", size_x= 12, size_y=12, size_z=0.1)
jipian = ElectrodeSheet(name=f"jipian1_{i}", size_x= 12, size_y=12, size_z=0.1)
liaopan1.children[i].assign_child_resource(jipian, location=None)
#
#创建一个4*4的物料板
liaopan2 = MaterialPlate(name="liaopan2", size_x=120.8, size_y=120.5, size_z=10.0, fill=True)
#把物料板放到桌子上
deck.assign_child_resource(liaopan2, Coordinate(x=500, y=0, z=0))
#创建一个4*4的物料板
liaopan3 = MaterialPlate(name="liaopan3", size_x=120.8, size_y=120.5, size_z=10.0, fill=True)
liaopan3 = MaterialPlate(name="电池料盘", size_x=120.8, size_y=160.5, size_z=10.0, fill=True)
#把物料板放到桌子上
deck.assign_child_resource(liaopan3, Coordinate(x=1000, y=0, z=0))
print(deck)
return deck
deck.assign_child_resource(liaopan3, Coordinate(x=100, y=100, z=0))
import json
if __name__ == "__main__":
electrode1 = BatteryPressSlot()
#print(electrode1.get_size_x())
#print(electrode1.get_size_y())
#print(electrode1.get_size_z())
#jipian = ElectrodeSheet()
#jipian._unilabos_state["diameter"] = 18
#print(jipian.serialize())
#print(jipian.serialize_state())
deck = CoincellDeck()
"""======================================子弹夹============================================"""
zip_dan_jia = ClipMagazine_four("zi_dan_jia", 80, 80, 10)
deck.assign_child_resource(zip_dan_jia, Coordinate(x=1400, y=50, z=0))
zip_dan_jia2 = ClipMagazine_four("zi_dan_jia2", 80, 80, 10)
deck.assign_child_resource(zip_dan_jia2, Coordinate(x=1600, y=200, z=0))
zip_dan_jia3 = ClipMagazine("zi_dan_jia3", 80, 80, 10)
deck.assign_child_resource(zip_dan_jia3, Coordinate(x=1500, y=200, z=0))
zip_dan_jia4 = ClipMagazine("zi_dan_jia4", 80, 80, 10)
deck.assign_child_resource(zip_dan_jia4, Coordinate(x=1500, y=300, z=0))
zip_dan_jia5 = ClipMagazine("zi_dan_jia5", 80, 80, 10)
deck.assign_child_resource(zip_dan_jia5, Coordinate(x=1600, y=300, z=0))
zip_dan_jia6 = ClipMagazine("zi_dan_jia6", 80, 80, 10)
deck.assign_child_resource(zip_dan_jia6, Coordinate(x=1530, y=500, z=0))
zip_dan_jia7 = ClipMagazine("zi_dan_jia7", 80, 80, 10)
deck.assign_child_resource(zip_dan_jia7, Coordinate(x=1180, y=400, z=0))
zip_dan_jia8 = ClipMagazine("zi_dan_jia8", 80, 80, 10)
deck.assign_child_resource(zip_dan_jia8, Coordinate(x=1280, y=400, z=0))
for i in range(4):
jipian = ElectrodeSheet(name=f"zi_dan_jia_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
zip_dan_jia2.children[i].assign_child_resource(jipian, location=None)
for i in range(4):
jipian2 = ElectrodeSheet(name=f"zi_dan_jia2_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
zip_dan_jia.children[i].assign_child_resource(jipian2, location=None)
for i in range(6):
jipian3 = ElectrodeSheet(name=f"zi_dan_jia3_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
zip_dan_jia3.children[i].assign_child_resource(jipian3, location=None)
for i in range(6):
jipian4 = ElectrodeSheet(name=f"zi_dan_jia4_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
zip_dan_jia4.children[i].assign_child_resource(jipian4, location=None)
for i in range(6):
jipian5 = ElectrodeSheet(name=f"zi_dan_jia5_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
zip_dan_jia5.children[i].assign_child_resource(jipian5, location=None)
for i in range(6):
jipian6 = ElectrodeSheet(name=f"zi_dan_jia6_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
zip_dan_jia6.children[i].assign_child_resource(jipian6, location=None)
for i in range(6):
jipian7 = ElectrodeSheet(name=f"zi_dan_jia7_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
zip_dan_jia7.children[i].assign_child_resource(jipian7, location=None)
for i in range(6):
jipian8 = ElectrodeSheet(name=f"zi_dan_jia8_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
zip_dan_jia8.children[i].assign_child_resource(jipian8, location=None)
"""======================================子弹夹============================================"""
#liaopan = TipBox64(name="liaopan")
"""======================================物料板============================================"""
#创建一个4*4的物料板
liaopan1 = MaterialPlate(name="liaopan1", size_x=120, size_y=100, size_z=10.0, fill=True)
deck.assign_child_resource(liaopan1, Coordinate(x=1010, y=50, z=0))
for i in range(16):
jipian_1 = ElectrodeSheet(name=f"{liaopan1.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
liaopan1.children[i].assign_child_resource(jipian_1, location=None)
liaopan2 = MaterialPlate(name="liaopan2", size_x=120, size_y=100, size_z=10.0, fill=True)
deck.assign_child_resource(liaopan2, Coordinate(x=1130, y=50, z=0))
liaopan3 = MaterialPlate(name="liaopan3", size_x=120, size_y=100, size_z=10.0, fill=True)
deck.assign_child_resource(liaopan3, Coordinate(x=1250, y=50, z=0))
liaopan4 = MaterialPlate(name="liaopan4", size_x=120, size_y=100, size_z=10.0, fill=True)
deck.assign_child_resource(liaopan4, Coordinate(x=1010, y=150, z=0))
for i in range(16):
jipian_4 = ElectrodeSheet(name=f"{liaopan4.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
liaopan4.children[i].assign_child_resource(jipian_4, location=None)
liaopan5 = MaterialPlate(name="liaopan5", size_x=120, size_y=100, size_z=10.0, fill=True)
deck.assign_child_resource(liaopan5, Coordinate(x=1130, y=150, z=0))
liaopan6 = MaterialPlate(name="liaopan6", size_x=120, size_y=100, size_z=10.0, fill=True)
deck.assign_child_resource(liaopan6, Coordinate(x=1250, y=150, z=0))
#liaopan.children[3].assign_child_resource(jipian, location=None)
"""======================================物料板============================================"""
"""======================================瓶架,移液枪============================================"""
# 在台面上放置 3x4 瓶架、6x2 瓶架 与 64孔移液枪头盒
bottle_rack_3x4 = BottleRack(
name="bottle_rack_3x4",
size_x=210.0,
size_y=140.0,
size_z=100.0,
num_items_x=3,
num_items_y=4,
position_spacing=35.0,
orientation="vertical",
)
deck.assign_child_resource(bottle_rack_3x4, Coordinate(x=100, y=200, z=0))
bottle_rack_6x2 = BottleRack(
name="bottle_rack_6x2",
size_x=120.0,
size_y=250.0,
size_z=100.0,
num_items_x=6,
num_items_y=2,
position_spacing=35.0,
orientation="vertical",
)
deck.assign_child_resource(bottle_rack_6x2, Coordinate(x=300, y=300, z=0))
bottle_rack_6x2_2 = BottleRack(
name="bottle_rack_6x2_2",
size_x=120.0,
size_y=250.0,
size_z=100.0,
num_items_x=6,
num_items_y=2,
position_spacing=35.0,
orientation="vertical",
)
deck.assign_child_resource(bottle_rack_6x2_2, Coordinate(x=430, y=300, z=0))
# 将 ElectrodeSheet 放满 3x4 与 6x2 的所有孔位
for idx in range(bottle_rack_3x4.num_items_x * bottle_rack_3x4.num_items_y):
sheet = ElectrodeSheet(name=f"sheet_3x4_{idx}", size_x=12, size_y=12, size_z=0.1)
bottle_rack_3x4.assign_child_resource(sheet, index=idx)
for idx in range(bottle_rack_6x2.num_items_x * bottle_rack_6x2.num_items_y):
sheet = ElectrodeSheet(name=f"sheet_6x2_{idx}", size_x=12, size_y=12, size_z=0.1)
bottle_rack_6x2.assign_child_resource(sheet, index=idx)
tip_box = TipBox64(name="tip_box_64")
deck.assign_child_resource(tip_box, Coordinate(x=300, y=100, z=0))
waste_tip_box = WasteTipBox(name="waste_tip_box")
deck.assign_child_resource(waste_tip_box, Coordinate(x=300, y=200, z=0))
"""======================================瓶架,移液枪============================================"""
print(deck)
from unilabos.resources.graphio import convert_resources_from_type
from unilabos.config.config import BasicConfig
BasicConfig.ak = "56bbed5b-6e30-438c-b06d-f69eaa63bb45"
BasicConfig.sk = "238222fe-0bf7-4350-a426-e5ced8011dcf"
BasicConfig.ak = "4d5ce6ae-7234-4639-834e-93899b9caf94"
BasicConfig.sk = "505d3b0a-620e-459a-9905-1efcffce382a"
from unilabos.app.web.client import http_client
resources = convert_resources_from_type([deck], [Resource])
# 检查序列化后的资源
json.dump({"nodes": resources, "links": []}, open("button_battery_decks_unilab.json", "w"), indent=2)
json.dump({"nodes": resources, "links": []}, open("button_battery_station_resources_unilab.json", "w"), indent=2)
#print(resources)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -21,7 +21,7 @@ from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode
class CoinCellAssemblyWorkstation(WorkstationBase):
def __init__(
self,
deck: CoincellDeck,
station_resource: CoincellDeck,
address: str = "192.168.1.20",
port: str = "502",
debug_mode: bool = True,
@@ -30,12 +30,12 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
):
super().__init__(
#桌子
deck=deck,
station_resource=station_resource,
*args,
**kwargs,
)
self.debug_mode = debug_mode
self.deck = deck
self.station_resource = station_resource
""" 连接初始化 """
modbus_client = TCPClient(addr=address, port=port)
print("modbus_client", modbus_client)
@@ -60,6 +60,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
self.csv_export_thread = None
self.csv_export_running = False
self.csv_export_file = None
self.coin_num_N = 0 #已组装电池数量
#创建一个物料台面,包含两个极片板
#self.deck = create_a_coin_cell_deck()
@@ -74,7 +75,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
self._ros_node = ros_node
#self.deck = create_a_coin_cell_deck()
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
"resources": [self.deck]
"resources": [self.station_resource]
})
# 批量操作在这里写
@@ -84,7 +85,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
async def fill_plate(self):
plate_1: MaterialPlate = self.deck.children[0].children[0]
plate_1: MaterialPlate = self.station_resource.children[0].children[0]
#plate_1
return await self._ros_node.update_resource(plate_1)
@@ -341,7 +342,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
def modify_deck_name(self, resource_name: str):
# figure_res = self._ros_node.resource_tracker.figure_resource({"name": resource_name})
# print(f"!!! figure_res: {type(figure_res)}")
self.deck.children[1]
self.station_resource.children[1]
return
@property
@@ -606,7 +607,8 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
print("waiting for start_cmd")
time.sleep(1)
def func_pack_send_bottle_num(self, bottle_num: int):
def func_pack_send_bottle_num(self, bottle_num):
bottle_num = int(bottle_num)
#发送电解液平台数
print("启动")
while (self._unilab_rece_electrolyte_bottle_num()) == False:
@@ -654,16 +656,25 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
# self.success = True
# return self.success
def func_pack_send_msg_cmd(self, elec_use_num) -> bool:
def func_pack_send_msg_cmd(self, elec_use_num, elec_vol, assembly_type, assembly_pressure) -> bool:
"""UNILAB写参数"""
while (self.request_rec_msg_status) == False:
print("wait for request_rec_msg_status to True")
time.sleep(1)
self.success = False
#self._unilab_send_msg_electrolyte_num(elec_num)
time.sleep(1)
#设置平行样数目
self._unilab_send_msg_electrolyte_use_num(elec_use_num)
time.sleep(1)
#发送电解液加注量
self._unilab_send_msg_electrolyte_vol(elec_vol)
time.sleep(1)
#发送电解液组装类型
self._unilab_send_msg_assembly_type(assembly_type)
time.sleep(1)
#发送电池压制力
self._unilab_send_msg_assembly_pressure(assembly_pressure)
time.sleep(1)
self._unilab_send_msg_succ_cmd(True)
time.sleep(1)
while (self.request_rec_msg_status) == True:
@@ -697,6 +708,23 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
print("data_electrolyte_code", data_electrolyte_code)
print("data_coin_cell_code", data_coin_cell_code)
#接收完信息后读取完毕标志位置True
liaopan3 = self.station_resource.get_resource("\u7535\u6c60\u6599\u76d8")
#把物料解绑后放到另一盘上
battery = ElectrodeSheet(name=f"battery_{self.coin_num_N}", size_x=14, size_y=14, size_z=2)
battery._unilabos_state = {
"electrolyte_name": data_coin_cell_code,
"data_electrolyte_code": data_electrolyte_code,
"open_circuit_voltage": data_open_circuit_voltage,
"assembly_pressure": data_assembly_pressure,
"electrolyte_volume": data_electrolyte_volume
}
liaopan3.children[self.coin_num_N].assign_child_resource(battery, location=None)
#print(jipian2.parent)
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
"resources": [self.station_resource]
})
self._unilab_rec_msg_succ_cmd(True)
time.sleep(1)
#等待允许读取标志位置False
@@ -756,7 +784,8 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
def func_allpack_cmd(self, elec_num, elec_use_num, file_path: str="D:\\coin_cell_data") -> bool:
def func_allpack_cmd(self, elec_num, elec_use_num, elec_vol:int=50, assembly_type:int=7, assembly_pressure:int=4200, file_path: str="D:\\coin_cell_data") -> bool:
elec_num, elec_use_num, elec_vol, assembly_type, assembly_pressure = int(elec_num), int(elec_use_num), int(elec_vol), int(assembly_type), int(assembly_pressure)
summary_csv_file = os.path.join(file_path, "duandian.csv")
# 如果断点文件存在,先读取之前的进度
if os.path.exists(summary_csv_file):
@@ -784,54 +813,38 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
elec_num_N = 0
elec_use_num_N = 0
coin_num_N = 0
print(f"剩余电解液瓶数: {elec_num}, 已组装电池数: {elec_use_num}")
for i in range(20):
print(f"剩余电解液瓶数: {elec_num}, 已组装电池数: {elec_use_num}")
print(f"剩余电解液瓶数: {type(elec_num)}, 已组装电池数: {type(elec_use_num)}")
print(f"剩余电解液瓶数: {type(int(elec_num))}, 已组装电池数: {type(int(elec_use_num))}")
#如果是第一次运行,则进行初始化、切换自动、启动, 如果是断点重启则跳过。
if read_status_flag == False:
pass
#初始化
self.func_pack_device_init()
#self.func_pack_device_init()
#切换自动
self.func_pack_device_auto()
#self.func_pack_device_auto()
#启动,小车收回
self.func_pack_device_start()
#self.func_pack_device_start()
#发送电解液瓶数量,启动搬运,多搬运没事
self.func_pack_send_bottle_num(elec_num)
#self.func_pack_send_bottle_num(elec_num)
last_i = elec_num_N
last_j = elec_use_num_N
for i in range(last_i, elec_num):
print(f"开始第{last_i+i+1}瓶电解液的组装")
#第一个循环从上次断点继续后续循环从0开始
j_start = last_j if i == last_i else 0
self.func_pack_send_msg_cmd(elec_use_num-j_start)
self.func_pack_send_msg_cmd(elec_use_num-j_start, elec_vol, assembly_type, assembly_pressure)
for j in range(j_start, elec_use_num):
print(f"开始第{last_i+i+1}瓶电解液的第{j+j_start+1}个电池组装")
#读取电池组装数据并存入csv
self.func_pack_get_msg_cmd(file_path)
time.sleep(1)
#这里定义物料系统
# TODO:读完再将电池数加一还是进入循环就将电池数加一需要考虑
liaopan1 = self.deck.get_resource("liaopan1")
liaopan4 = self.deck.get_resource("liaopan4")
jipian1 = liaopan1.children[coin_num_N].children[0]
jipian4 = liaopan4.children[coin_num_N].children[0]
#print(jipian1)
#从料盘上去物料解绑后放到另一盘上
jipian1.parent.unassign_child_resource(jipian1)
jipian4.parent.unassign_child_resource(jipian4)
#print(jipian2.parent)
battery = Battery(name = f"battery_{coin_num_N}")
battery.assign_child_resource(jipian1, location=None)
battery.assign_child_resource(jipian4, location=None)
zidanjia6 = self.deck.get_resource("zi_dan_jia6")
zidanjia6.children[0].assign_child_resource(battery, location=None)
# 生成断点文件
# 生成包含elec_num_N、coin_num_N、timestamp的CSV文件
@@ -842,6 +855,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
writer.writerow([elec_num, elec_use_num, elec_num_N, elec_use_num_N, coin_num_N, timestamp])
csvfile.flush()
coin_num_N += 1
self.coin_num_N = coin_num_N
elec_use_num_N += 1
elec_num_N += 1
elec_use_num_N = 0
@@ -878,34 +892,22 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
def fun_wuliao_test(self) -> bool:
#找到data_init中构建的2个物料盘
#liaopan1 = self.deck.get_resource("liaopan1")
#liaopan4 = self.deck.get_resource("liaopan4")
#for coin_num_N in range(16):
# liaopan1 = self.deck.get_resource("liaopan1")
# liaopan4 = self.deck.get_resource("liaopan4")
# jipian1 = liaopan1.children[coin_num_N].children[0]
# jipian4 = liaopan4.children[coin_num_N].children[0]
# #print(jipian1)
# #从料盘上去物料解绑后放到另一盘上
# jipian1.parent.unassign_child_resource(jipian1)
# jipian4.parent.unassign_child_resource(jipian4)
#
# #print(jipian2.parent)
# battery = Battery(name = f"battery_{coin_num_N}")
# battery.assign_child_resource(jipian1, location=None)
# battery.assign_child_resource(jipian4, location=None)
#
# zidanjia6 = self.deck.get_resource("zi_dan_jia6")
# zidanjia6.children[0].assign_child_resource(battery, location=None)
# ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
# "resources": [self.deck]
# })
# time.sleep(2)
for i in range(20):
print(f"输出{i}")
time.sleep(2)
liaopan3 = self.station_resource.get_resource("\u7535\u6c60\u6599\u76d8")
for i in range(16):
battery = ElectrodeSheet(name=f"battery_{i}", size_x=16, size_y=16, size_z=2)
battery._unilabos_state = {
"diameter": 20.0,
"height": 20.0,
"assembly_pressure": i,
"electrolyte_volume": 20.0,
"electrolyte_name": f"DP{i}"
}
liaopan3.children[i].assign_child_resource(battery, location=None)
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
"resources": [self.station_resource]
})
time.sleep(4)
# 数据读取与输出
def func_read_data_and_output(self, file_path: str="D:\\coin_cell_data"):
# 检查CSV导出是否正在运行已运行则跳出防止同时启动两个while循环
@@ -1119,24 +1121,52 @@ if __name__ == "__main__":
#print("success")
#创建一个物料台面
#deck = create_a_coin_cell_deck()
deck = create_a_coin_cell_deck()
#deck = create_a_full_coin_cell_deck()
##在台面上找到料盘和极片
#liaopan1 = deck.get_resource("liaopan1")
#liaopan2 = deck.get_resource("liaopan2")
#jipian1 = liaopan1.children[1].children[0]
#
##print(jipian1)
##
#print(jipian1)
##把物料解绑后放到另一盘上
#jipian1.parent.unassign_child_resource(jipian1)
#liaopan2.children[1].assign_child_resource(jipian1, location=None)
##print(jipian2.parent)
liaopan1 = deck.get_resource("liaopan1")
liaopan2 = deck.get_resource("liaopan2")
for i in range(16):
#找到liaopan1上每一个jipian
jipian_linshi = liaopan1.children[i].children[0]
#把物料解绑后放到另一盘上
print("极片:", jipian_linshi)
jipian_linshi.parent.unassign_child_resource(jipian_linshi)
liaopan2.children[i].assign_child_resource(jipian_linshi, location=None)
from unilabos.resources.graphio import resource_ulab_to_plr, convert_resources_to_type
#with open("./button_battery_station_resources_unilab.json", "r", encoding="utf-8") as f:
# bioyond_resources_unilab = json.load(f)
#print(f"成功读取 JSON 文件,包含 {len(bioyond_resources_unilab)} 个资源")
#ulab_resources = convert_resources_to_type(bioyond_resources_unilab, List[PLRResource])
#print(f"转换结果类型: {type(ulab_resources)}")
#print(ulab_resources)
with open("./button_battery_decks_unilab.json", "r", encoding="utf-8") as f:
bioyond_resources_unilab = json.load(f)
print(f"成功读取 JSON 文件,包含 {len(bioyond_resources_unilab)} 个资源")
ulab_resources = convert_resources_to_type(bioyond_resources_unilab, List[PLRResource])
print(f"转换结果类型: {type(ulab_resources)}")
print(ulab_resources)
from unilabos.resources.graphio import convert_resources_from_type
from unilabos.config.config import BasicConfig
BasicConfig.ak = "beb0c15f-2279-46a1-aba5-00eaf89aef55"
BasicConfig.sk = "15d4f25e-3512-4f9c-9bfb-43ab85e7b561"
from unilabos.app.web.client import http_client
resources = convert_resources_from_type([deck], [Resource])
json.dump({"nodes": resources, "links": []}, open("button_battery_station_resources_unilab.json", "w"), indent=2)
#print(resources)
http_client.remote_addr = "https://uni-lab.test.bohrium.com/api/v1"
http_client.resource_add(resources)

View File

@@ -0,0 +1,44 @@
Name,DataType,InitValue,Comment,Attribute,DeviceType,Address
COIL_SYS_START_CMD,BOOL,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,coil,8010
COIL_SYS_STOP_CMD,BOOL,,<EFBFBD>豸ֹͣ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,coil,8020
COIL_SYS_RESET_CMD,BOOL,,<EFBFBD><EFBFBD><EFBFBD>λ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,coil,8030
COIL_SYS_HAND_CMD,BOOL,,<EFBFBD><EFBFBD>ֶ<EFBFBD>ģʽ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,coil,8040
COIL_SYS_AUTO_CMD,BOOL,,<EFBFBD><EFBFBD>Զ<EFBFBD>ģʽ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,coil,8050
COIL_SYS_INIT_CMD,BOOL,,<EFBFBD><EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD>ģʽ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,coil,8060
COIL_UNILAB_SEND_MSG_SUCC_CMD,BOOL,,UNILAB<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,coil,8700
COIL_UNILAB_REC_MSG_SUCC_CMD,BOOL,,UNILAB<EFBFBD><EFBFBD><EFBFBD>ܲ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,coil,8710
COIL_SYS_START_STATUS,BOOL,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,coil,8210
COIL_SYS_STOP_STATUS,BOOL,,<EFBFBD>豸ֹͣ<EFBFBD><EFBFBD>,,coil,8220
COIL_SYS_RESET_STATUS,BOOL,,<EFBFBD><EFBFBD><EFBFBD>λ<EFBFBD><EFBFBD>,,coil,8230
COIL_SYS_HAND_STATUS,BOOL,,<EFBFBD><EFBFBD>ֶ<EFBFBD>ģʽ,,coil,8240
COIL_SYS_AUTO_STATUS,BOOL,,<EFBFBD><EFBFBD>Զ<EFBFBD>ģʽ,,coil,8250
COIL_SYS_INIT_STATUS,BOOL,,<EFBFBD><EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,coil,8260
COIL_REQUEST_REC_MSG_STATUS,BOOL,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,coil,8510
COIL_REQUEST_SEND_MSG_STATUS,BOOL,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ͳ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,coil,8500
REG_MSG_ELECTROLYTE_USE_NUM,INT16,,<EFBFBD><EFBFBD>ƿ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Һʹ<EFBFBD>ô<EFBFBD><EFBFBD><EFBFBD>,,hold_register,11000
REG_MSG_ELECTROLYTE_NUM,INT16,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Һʹ<EFBFBD><EFBFBD>ƿ<EFBFBD><EFBFBD>,,hold_register,11002
REG_MSG_ELECTROLYTE_VOLUME,INT16,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Һ<EFBFBD><EFBFBD>ȡ<EFBFBD><EFBFBD>,,hold_register,11004
REG_MSG_ASSEMBLY_TYPE,INT16,,<EFBFBD><EFBFBD>װ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƭ<EFBFBD>ѵ<EFBFBD><EFBFBD><EFBFBD>ʽ,,hold_register,11006
REG_MSG_ASSEMBLY_PRESSURE,INT16,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>װѹ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,hold_register,11008
REG_DATA_ASSEMBLY_COIN_CELL_NUM,INT16,,<EFBFBD><EFBFBD>ǰ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>װ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,hold_register,10000
REG_DATA_OPEN_CIRCUIT_VOLTAGE,FLOAT32,,<EFBFBD><EFBFBD>ǰ<EFBFBD><EFBFBD><EFBFBD>ص<EFBFBD>ѹ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,hold_register,10002
REG_DATA_AXIS_X_POS,FLOAT32,,<EFBFBD><EFBFBD>ҺX<EFBFBD>ᵱǰλ<EFBFBD><EFBFBD>,,hold_register,10004
REG_DATA_AXIS_Y_POS,FLOAT32,,<EFBFBD><EFBFBD>ҺZ<EFBFBD>ᵱǰλ<EFBFBD><EFBFBD>,,hold_register,10006
REG_DATA_AXIS_Z_POS,FLOAT32,,<EFBFBD><EFBFBD>ҺY<EFBFBD>ᵱǰλ<EFBFBD><EFBFBD>,,hold_register,10008
REG_DATA_POLE_WEIGHT,FLOAT32,,<EFBFBD><EFBFBD>ǰ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƭ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,hold_register,10010
REG_DATA_ASSEMBLY_PER_TIME,FLOAT32,,<EFBFBD><EFBFBD>ǰ<EFBFBD><EFBFBD><EFBFBD>ŵ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>װʱ<EFBFBD><EFBFBD>,,hold_register,10012
REG_DATA_ASSEMBLY_PRESSURE,INT16,,<EFBFBD><EFBFBD>ǰ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>װѹ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,hold_register,10014
REG_DATA_ELECTROLYTE_VOLUME,INT16,,<EFBFBD><EFBFBD>ǰ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Һ<EFBFBD><EFBFBD>ע<EFBFBD><EFBFBD>,,hold_register,10016
REG_DATA_COIN_NUM,INT16,,<EFBFBD><EFBFBD>ǰ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,hold_register,10018
REG_DATA_ELECTROLYTE_CODE,STRING,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Һ<EFBFBD><EFBFBD>ά<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>к<EFBFBD>,,hold_register,10020
REG_DATA_COIN_CELL_CODE,STRING,,<EFBFBD><EFBFBD><EFBFBD>ض<EFBFBD>ά<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>к<EFBFBD>,,hold_register,10030
REG_DATA_STACK_VISON_CODE,STRING,,<EFBFBD><EFBFBD><EFBFBD>϶ѵ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͼƬ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,hold_register,12004
REG_DATA_GLOVE_BOX_PRESSURE,FLOAT32,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѹ<EFBFBD><EFBFBD>,,hold_register,10050
REG_DATA_GLOVE_BOX_WATER_CONTENT,FLOAT32,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ˮ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,hold_register,10052
REG_DATA_GLOVE_BOX_O2_CONTENT,FLOAT32,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,hold_register,10054
UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM,BOOL,,Unilabȷ<EFBFBD><EFBFBD><EFBFBD>ѷ<EFBFBD><EFBFBD>͵<EFBFBD><EFBFBD><EFBFBD>Һƿ<EFBFBD><EFBFBD><EFBFBD>ź<EFBFBD>,,coil,8720
UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM,BOOL,,Unilab<EFBFBD>ɽ<EFBFBD><EFBFBD>ܵ<EFBFBD><EFBFBD><EFBFBD>Һƿ<EFBFBD><EFBFBD>,,coil,8520
REG_MSG_ELECTROLYTE_NUM_USED,INT16,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Һ<EFBFBD><EFBFBD>װ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ƽ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,hold_register,496
REG_DATA_ELECTROLYTE_USE_NUM,INT16,,<EFBFBD><EFBFBD>ǰ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>װƽ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,hold_register,10000
UNILAB_SEND_FINISHED_CMD,BOOL,,Unilab<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>յ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ź<EFBFBD>,,coil,8730
UNILAB_RECE_FINISHED_CMD,BOOL,,<EFBFBD><EFBFBD>֪unilab<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ź<EFBFBD>,,coil,8530
1 Name DataType InitValue Comment Attribute DeviceType Address
2 COIL_SYS_START_CMD BOOL 设备启动命令 coil 8010
3 COIL_SYS_STOP_CMD BOOL 设备停止命令 coil 8020
4 COIL_SYS_RESET_CMD BOOL 设备复位命令 coil 8030
5 COIL_SYS_HAND_CMD BOOL 设备手动模式命令 coil 8040
6 COIL_SYS_AUTO_CMD BOOL 设备自动模式命令 coil 8050
7 COIL_SYS_INIT_CMD BOOL 设备初始化模式命令 coil 8060
8 COIL_UNILAB_SEND_MSG_SUCC_CMD BOOL UNILAB发送配方完毕 coil 8700
9 COIL_UNILAB_REC_MSG_SUCC_CMD BOOL UNILAB接受测试数据完毕 coil 8710
10 COIL_SYS_START_STATUS BOOL 设备启动中 coil 8210
11 COIL_SYS_STOP_STATUS BOOL 设备停止中 coil 8220
12 COIL_SYS_RESET_STATUS BOOL 设备复位中 coil 8230
13 COIL_SYS_HAND_STATUS BOOL 设备手动模式 coil 8240
14 COIL_SYS_AUTO_STATUS BOOL 设备自动模式 coil 8250
15 COIL_SYS_INIT_STATUS BOOL 设备初始化完成 coil 8260
16 COIL_REQUEST_REC_MSG_STATUS BOOL 设备请求接受配方 coil 8510
17 COIL_REQUEST_SEND_MSG_STATUS BOOL 设备请求发送测试数据 coil 8500
18 REG_MSG_ELECTROLYTE_USE_NUM INT16 单瓶电解液使用次数 hold_register 11000
19 REG_MSG_ELECTROLYTE_NUM INT16 电解液使用瓶数 hold_register 11002
20 REG_MSG_ELECTROLYTE_VOLUME INT16 电解液吸取量 hold_register 11004
21 REG_MSG_ASSEMBLY_TYPE INT16 组装参数:极片堆叠方式 hold_register 11006
22 REG_MSG_ASSEMBLY_PRESSURE INT16 电池组装压制力 hold_register 11008
23 REG_DATA_ASSEMBLY_COIN_CELL_NUM INT16 当前完成组装电池数量 hold_register 10000
24 REG_DATA_OPEN_CIRCUIT_VOLTAGE FLOAT32 当前电池电压数据 hold_register 10002
25 REG_DATA_AXIS_X_POS FLOAT32 分液X轴当前位置 hold_register 10004
26 REG_DATA_AXIS_Y_POS FLOAT32 分液Z轴当前位置 hold_register 10006
27 REG_DATA_AXIS_Z_POS FLOAT32 分液Y轴当前位置 hold_register 10008
28 REG_DATA_POLE_WEIGHT FLOAT32 当前电池正极片称重数据 hold_register 10010
29 REG_DATA_ASSEMBLY_PER_TIME FLOAT32 当前单颗电池组装时间 hold_register 10012
30 REG_DATA_ASSEMBLY_PRESSURE INT16 当前电池组装压制力 hold_register 10014
31 REG_DATA_ELECTROLYTE_VOLUME INT16 当前电解液加注量 hold_register 10016
32 REG_DATA_COIN_NUM INT16 当前电池物料数 hold_register 10018
33 REG_DATA_ELECTROLYTE_CODE STRING 电解液二维码序列号 hold_register 10020
34 REG_DATA_COIN_CELL_CODE STRING 电池二维码序列号 hold_register 10030
35 REG_DATA_STACK_VISON_CODE STRING 物料堆叠复检图片编码 hold_register 12004
36 REG_DATA_GLOVE_BOX_PRESSURE FLOAT32 手套箱压力 hold_register 10050
37 REG_DATA_GLOVE_BOX_WATER_CONTENT FLOAT32 手套箱水含量 hold_register 10052
38 REG_DATA_GLOVE_BOX_O2_CONTENT FLOAT32 手套箱氧含量 hold_register 10054
39 UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM BOOL Unilab确认已发送电解液瓶数信号 coil 8720
40 UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM BOOL Unilab可接受电解液瓶数 coil 8520
41 REG_MSG_ELECTROLYTE_NUM_USED INT16 电解液组装电池平行样数 hold_register 496
42 REG_DATA_ELECTROLYTE_USE_NUM INT16 当前已组装平行样数 hold_register 10000
43 UNILAB_SEND_FINISHED_CMD BOOL Unilab发送收到完成信号 coil 8730
44 UNILAB_RECE_FINISHED_CMD BOOL 告知unilab结束信号 coil 8530

View File

@@ -0,0 +1,46 @@
Name,DataType,InitValue,Comment,Attribute,DeviceType,Address,
COIL_SYS_START_CMD,BOOL,,,,coil,8010,
COIL_SYS_STOP_CMD,BOOL,,,,coil,8020,
COIL_SYS_RESET_CMD,BOOL,,,,coil,8030,
COIL_SYS_HAND_CMD,BOOL,,,,coil,8040,
COIL_SYS_AUTO_CMD,BOOL,,,,coil,8050,
COIL_SYS_INIT_CMD,BOOL,,,,coil,8060,
COIL_UNILAB_SEND_MSG_SUCC_CMD,BOOL,,,,coil,8700,
COIL_UNILAB_REC_MSG_SUCC_CMD,BOOL,,,,coil,8710,unilab_rec_msg_succ_cmd
COIL_SYS_START_STATUS,BOOL,,,,coil,8210,
COIL_SYS_STOP_STATUS,BOOL,,,,coil,8220,
COIL_SYS_RESET_STATUS,BOOL,,,,coil,8230,
COIL_SYS_HAND_STATUS,BOOL,,,,coil,8240,
COIL_SYS_AUTO_STATUS,BOOL,,,,coil,8250,
COIL_SYS_INIT_STATUS,BOOL,,,,coil,8260,
COIL_REQUEST_REC_MSG_STATUS,BOOL,,,,coil,8500,
COIL_REQUEST_SEND_MSG_STATUS,BOOL,,,,coil,8510,request_send_msg_status
REG_MSG_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,11000,
REG_MSG_ELECTROLYTE_NUM,INT16,,,,hold_register,11002,unilab_send_msg_electrolyte_num
REG_MSG_ELECTROLYTE_VOLUME,INT16,,,,hold_register,11004,unilab_send_msg_electrolyte_vol
REG_MSG_ASSEMBLY_TYPE,INT16,,,,hold_register,11006,unilab_send_msg_assembly_type
REG_MSG_ASSEMBLY_PRESSURE,INT16,,,,hold_register,11008,unilab_send_msg_assembly_pressure
REG_DATA_ASSEMBLY_COIN_CELL_NUM,INT16,,,,hold_register,10000,data_assembly_coin_cell_num
REG_DATA_OPEN_CIRCUIT_VOLTAGE,FLOAT32,,,,hold_register,10002,data_open_circuit_voltage
REG_DATA_AXIS_X_POS,FLOAT32,,,,hold_register,10004,
REG_DATA_AXIS_Y_POS,FLOAT32,,,,hold_register,10006,
REG_DATA_AXIS_Z_POS,FLOAT32,,,,hold_register,10008,
REG_DATA_POLE_WEIGHT,FLOAT32,,,,hold_register,10010,data_pole_weight
REG_DATA_ASSEMBLY_PER_TIME,FLOAT32,,,,hold_register,10012,data_assembly_time
REG_DATA_ASSEMBLY_PRESSURE,INT16,,,,hold_register,10014,data_assembly_pressure
REG_DATA_ELECTROLYTE_VOLUME,INT16,,,,hold_register,10016,data_electrolyte_volume
REG_DATA_COIN_NUM,INT16,,,,hold_register,10018,data_coin_num
REG_DATA_ELECTROLYTE_CODE,STRING,,,,hold_register,10020,data_electrolyte_code()
REG_DATA_COIN_CELL_CODE,STRING,,,,hold_register,10030,data_coin_cell_code()
REG_DATA_STACK_VISON_CODE,STRING,,,,hold_register,12004,data_stack_vision_code()
REG_DATA_GLOVE_BOX_PRESSURE,FLOAT32,,,,hold_register,10050,data_glove_box_pressure
REG_DATA_GLOVE_BOX_WATER_CONTENT,FLOAT32,,,,hold_register,10052,data_glove_box_water_content
REG_DATA_GLOVE_BOX_O2_CONTENT,FLOAT32,,,,hold_register,10054,data_glove_box_o2_content
UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,8720,
UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,8520,
REG_MSG_ELECTROLYTE_NUM_USED,INT16,,,,hold_register,496,
REG_DATA_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,10000,
UNILAB_SEND_FINISHED_CMD,BOOL,,,,coil,8730,
UNILAB_RECE_FINISHED_CMD,BOOL,,,,coil,8530,
REG_DATA_ASSEMBLY_TYPE,INT16,,,,hold_register,10018,ASSEMBLY_TYPE7or8
COIL_ALUMINUM_FOIL,BOOL,,,,coil,8340,
1 Name DataType InitValue Comment Attribute DeviceType Address
2 COIL_SYS_START_CMD BOOL coil 8010
3 COIL_SYS_STOP_CMD BOOL coil 8020
4 COIL_SYS_RESET_CMD BOOL coil 8030
5 COIL_SYS_HAND_CMD BOOL coil 8040
6 COIL_SYS_AUTO_CMD BOOL coil 8050
7 COIL_SYS_INIT_CMD BOOL coil 8060
8 COIL_UNILAB_SEND_MSG_SUCC_CMD BOOL coil 8700
9 COIL_UNILAB_REC_MSG_SUCC_CMD BOOL coil 8710 unilab_rec_msg_succ_cmd
10 COIL_SYS_START_STATUS BOOL coil 8210
11 COIL_SYS_STOP_STATUS BOOL coil 8220
12 COIL_SYS_RESET_STATUS BOOL coil 8230
13 COIL_SYS_HAND_STATUS BOOL coil 8240
14 COIL_SYS_AUTO_STATUS BOOL coil 8250
15 COIL_SYS_INIT_STATUS BOOL coil 8260
16 COIL_REQUEST_REC_MSG_STATUS BOOL coil 8500
17 COIL_REQUEST_SEND_MSG_STATUS BOOL coil 8510 request_send_msg_status
18 REG_MSG_ELECTROLYTE_USE_NUM INT16 hold_register 11000
19 REG_MSG_ELECTROLYTE_NUM INT16 hold_register 11002 unilab_send_msg_electrolyte_num
20 REG_MSG_ELECTROLYTE_VOLUME INT16 hold_register 11004 unilab_send_msg_electrolyte_vol
21 REG_MSG_ASSEMBLY_TYPE INT16 hold_register 11006 unilab_send_msg_assembly_type
22 REG_MSG_ASSEMBLY_PRESSURE INT16 hold_register 11008 unilab_send_msg_assembly_pressure
23 REG_DATA_ASSEMBLY_COIN_CELL_NUM INT16 hold_register 10000 data_assembly_coin_cell_num
24 REG_DATA_OPEN_CIRCUIT_VOLTAGE FLOAT32 hold_register 10002 data_open_circuit_voltage
25 REG_DATA_AXIS_X_POS FLOAT32 hold_register 10004
26 REG_DATA_AXIS_Y_POS FLOAT32 hold_register 10006
27 REG_DATA_AXIS_Z_POS FLOAT32 hold_register 10008
28 REG_DATA_POLE_WEIGHT FLOAT32 hold_register 10010 data_pole_weight
29 REG_DATA_ASSEMBLY_PER_TIME FLOAT32 hold_register 10012 data_assembly_time
30 REG_DATA_ASSEMBLY_PRESSURE INT16 hold_register 10014 data_assembly_pressure
31 REG_DATA_ELECTROLYTE_VOLUME INT16 hold_register 10016 data_electrolyte_volume
32 REG_DATA_COIN_NUM INT16 hold_register 10018 data_coin_num
33 REG_DATA_ELECTROLYTE_CODE STRING hold_register 10020 data_electrolyte_code()
34 REG_DATA_COIN_CELL_CODE STRING hold_register 10030 data_coin_cell_code()
35 REG_DATA_STACK_VISON_CODE STRING hold_register 12004 data_stack_vision_code()
36 REG_DATA_GLOVE_BOX_PRESSURE FLOAT32 hold_register 10050 data_glove_box_pressure
37 REG_DATA_GLOVE_BOX_WATER_CONTENT FLOAT32 hold_register 10052 data_glove_box_water_content
38 REG_DATA_GLOVE_BOX_O2_CONTENT FLOAT32 hold_register 10054 data_glove_box_o2_content
39 UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM BOOL coil 8720
40 UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM BOOL coil 8520
41 REG_MSG_ELECTROLYTE_NUM_USED INT16 hold_register 496
42 REG_DATA_ELECTROLYTE_USE_NUM INT16 hold_register 10000
43 UNILAB_SEND_FINISHED_CMD BOOL coil 8730
44 UNILAB_RECE_FINISHED_CMD BOOL coil 8530
45 REG_DATA_ASSEMBLY_TYPE INT16 hold_register 10018 ASSEMBLY_TYPE7or8
46 COIL_ALUMINUM_FOIL BOOL coil 8340

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,691 @@
{
"nodes": [
{
"id": "BatteryStation",
"name": "扣电工作站",
"children": [
"coin_cell_deck"
],
"parent": null,
"type": "device",
"class": "bettery_station_registry",
"position": {
"x": 600,
"y": 400,
"z": 0
},
"config": {
"debug_mode": false,
"_comment": "protocol_type接外部工站固定写法字段一般为空station_resource写法也固定",
"protocol_type": [],
"station_resource": {
"data": {
"_resource_child_name": "coin_cell_deck",
"_resource_type": "unilabos.devices.workstation.coin_cell_assembly.button_battery_station:CoincellDeck"
}
},
"address": "192.168.1.20",
"port": 502
},
"data": {}
},
{
"id": "coin_cell_deck",
"name": "coin_cell_deck",
"sample_id": null,
"children": [
"\u7535\u6c60\u6599\u76d8"
],
"parent": null,
"type": "container",
"class": "",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"type": "CoincellDeck",
"size_x": 1000,
"size_y": 1000,
"size_z": 900,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "coin_cell_deck",
"barcode": null
},
"data": {}
},
{
"id": "\u7535\u6c60\u6599\u76d8",
"name": "\u7535\u6c60\u6599\u76d8",
"sample_id": null,
"children": [
"\u7535\u6c60\u6599\u76d8_materialhole_0_0",
"\u7535\u6c60\u6599\u76d8_materialhole_0_1",
"\u7535\u6c60\u6599\u76d8_materialhole_0_2",
"\u7535\u6c60\u6599\u76d8_materialhole_0_3",
"\u7535\u6c60\u6599\u76d8_materialhole_1_0",
"\u7535\u6c60\u6599\u76d8_materialhole_1_1",
"\u7535\u6c60\u6599\u76d8_materialhole_1_2",
"\u7535\u6c60\u6599\u76d8_materialhole_1_3",
"\u7535\u6c60\u6599\u76d8_materialhole_2_0",
"\u7535\u6c60\u6599\u76d8_materialhole_2_1",
"\u7535\u6c60\u6599\u76d8_materialhole_2_2",
"\u7535\u6c60\u6599\u76d8_materialhole_2_3",
"\u7535\u6c60\u6599\u76d8_materialhole_3_0",
"\u7535\u6c60\u6599\u76d8_materialhole_3_1",
"\u7535\u6c60\u6599\u76d8_materialhole_3_2",
"\u7535\u6c60\u6599\u76d8_materialhole_3_3"
],
"parent": "coin_cell_deck",
"type": "container",
"class": "",
"position": {
"x": 100,
"y": 100,
"z": 0
},
"config": {
"type": "MaterialPlate",
"size_x": 120.8,
"size_y": 160.5,
"size_z": 10.0,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_plate",
"model": null,
"barcode": null,
"ordering": {
"A1": "\u7535\u6c60\u6599\u76d8_materialhole_0_0",
"B1": "\u7535\u6c60\u6599\u76d8_materialhole_0_1",
"C1": "\u7535\u6c60\u6599\u76d8_materialhole_0_2",
"D1": "\u7535\u6c60\u6599\u76d8_materialhole_0_3",
"A2": "\u7535\u6c60\u6599\u76d8_materialhole_1_0",
"B2": "\u7535\u6c60\u6599\u76d8_materialhole_1_1",
"C2": "\u7535\u6c60\u6599\u76d8_materialhole_1_2",
"D2": "\u7535\u6c60\u6599\u76d8_materialhole_1_3",
"A3": "\u7535\u6c60\u6599\u76d8_materialhole_2_0",
"B3": "\u7535\u6c60\u6599\u76d8_materialhole_2_1",
"C3": "\u7535\u6c60\u6599\u76d8_materialhole_2_2",
"D3": "\u7535\u6c60\u6599\u76d8_materialhole_2_3",
"A4": "\u7535\u6c60\u6599\u76d8_materialhole_3_0",
"B4": "\u7535\u6c60\u6599\u76d8_materialhole_3_1",
"C4": "\u7535\u6c60\u6599\u76d8_materialhole_3_2",
"D4": "\u7535\u6c60\u6599\u76d8_materialhole_3_3"
}
},
"data": {}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_0_0",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_0_0",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 12.4,
"y": 104.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_0_1",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_0_1",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 12.4,
"y": 80.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_0_2",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_0_2",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 12.4,
"y": 56.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_0_3",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_0_3",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 12.4,
"y": 32.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_1_0",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_1_0",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 36.4,
"y": 104.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_1_1",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_1_1",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 36.4,
"y": 80.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_1_2",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_1_2",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 36.4,
"y": 56.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_1_3",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_1_3",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 36.4,
"y": 32.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_2_0",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_2_0",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 60.4,
"y": 104.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_2_1",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_2_1",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 60.4,
"y": 80.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_2_2",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_2_2",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 60.4,
"y": 56.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_2_3",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_2_3",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 60.4,
"y": 32.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_3_0",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_3_0",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 84.4,
"y": 104.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_3_1",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_3_1",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 84.4,
"y": 80.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_3_2",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_3_2",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 84.4,
"y": 56.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_3_3",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_3_3",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 84.4,
"y": 32.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
}
],
"links": []
}

View File

@@ -16,9 +16,9 @@
},
"config": {
"debug_mode": false,
"_comment": "protocol_type接外部工站固定写法字段一般为空deck写法也固定",
"_comment": "protocol_type接外部工站固定写法字段一般为空station_resource写法也固定",
"protocol_type": [],
"deck": {
"station_resource": {
"data": {
"_resource_child_name": "coin_cell_deck",
"_resource_type": "unilabos.devices.workstation.coin_cell_assembly.button_battery_station:CoincellDeck"

File diff suppressed because it is too large Load Diff

View File

@@ -112,17 +112,17 @@ class ResourceSynchronizer(ABC):
self.workstation = workstation
@abstractmethod
def sync_from_external(self) -> bool:
async def sync_from_external(self) -> bool:
"""从外部系统同步物料到本地deck"""
pass
@abstractmethod
def sync_to_external(self, plr_resource: PLRResource) -> bool:
async def sync_to_external(self, plr_resource: PLRResource) -> bool:
"""将本地物料同步到外部系统"""
pass
@abstractmethod
def handle_external_change(self, change_info: Dict[str, Any]) -> bool:
async def handle_external_change(self, change_info: Dict[str, Any]) -> bool:
"""处理外部系统的变更通知"""
pass
@@ -147,15 +147,21 @@ class WorkstationBase(ABC):
def __init__(
self,
deck: Deck,
station_resource: PLRResource,
*args,
**kwargs, # 必须有kwargs
):
# 基本配置
print(station_resource)
self.deck_config = station_resource
# PLR 物料系统
self.deck: Optional[Deck] = deck
self.deck: Optional[Deck] = None
self.plr_resources: Dict[str, PLRResource] = {}
self.resource_synchronizer = None # type: Optional[ResourceSynchronizer]
# 资源同步器(可选)
# self.resource_synchronizer = ResourceSynchronizer(self) # 要在driver中自行初始化只有workstation用
# 硬件接口
self.hardware_interface: Union[Any, str] = None
@@ -168,10 +174,97 @@ class WorkstationBase(ABC):
# 支持的工作流(静态预定义)
self.supported_workflows: Dict[str, WorkflowInfo] = {}
def post_init(self, ros_node: ROS2WorkstationNode) -> None:
# 初始化物料系统
self._ros_node = ros_node
self._ros_node.update_resource([self.deck])
self._initialize_material_system()
# 注册支持的工作流
# self._register_supported_workflows()
# logger.info(f"工作站 {device_id} 初始化完成(简化版)")
def _initialize_material_system(self):
"""初始化物料系统 - 使用 graphio 转换"""
try:
from unilabos.resources.graphio import resource_ulab_to_plr
# # 1. 合并 deck_config 和 children 创建完整的资源树
# complete_resource_config = self._create_complete_resource_config()
# # 2. 使用 graphio 转换为 PLR 资源
# self.deck = resource_ulab_to_plr(complete_resource_config, plr_model=True)
# # 3. 建立资源映射
# self._build_resource_mappings(self.deck)
# # 4. 如果有资源同步器,执行初始同步
# if self.resource_synchronizer:
# # 这里可以异步执行,暂时跳过
# pass
# logger.info(f"工作站 {self.device_id} 物料系统初始化成功,创建了 {len(self.plr_resources)} 个资源")
pass
except Exception as e:
# logger.error(f"工作站 {self.device_id} 物料系统初始化失败: {e}")
raise
def _create_complete_resource_config(self) -> Dict[str, Any]:
"""创建完整的资源配置 - 合并 deck_config 和 children"""
# 创建主 deck 配置
deck_resource = {
"id": f"{self.device_id}_deck",
"name": f"{self.device_id}_deck",
"type": "deck",
"position": {"x": 0, "y": 0, "z": 0},
"config": {
"size_x": self.deck_config.get("size_x", 1000.0),
"size_y": self.deck_config.get("size_y", 1000.0),
"size_z": self.deck_config.get("size_z", 100.0),
**{k: v for k, v in self.deck_config.items() if k not in ["size_x", "size_y", "size_z"]},
},
"data": {},
"children": [],
"parent": None,
}
# 添加子资源
if self._children:
children_list = []
for child_id, child_config in self._children.items():
child_resource = self._normalize_child_resource(child_id, child_config, deck_resource["id"])
children_list.append(child_resource)
deck_resource["children"] = children_list
return deck_resource
def _normalize_child_resource(self, resource_id: str, config: Dict[str, Any], parent_id: str) -> Dict[str, Any]:
"""标准化子资源配置"""
return {
"id": resource_id,
"name": config.get("name", resource_id),
"type": config.get("type", "container"),
"position": self._normalize_position(config.get("position", {})),
"config": config.get("config", {}),
"data": config.get("data", {}),
"children": [], # 简化版本:只支持一层子资源
"parent": parent_id,
}
def _normalize_position(self, position: Any) -> Dict[str, float]:
"""标准化位置信息"""
if isinstance(position, dict):
return {
"x": float(position.get("x", 0)),
"y": float(position.get("y", 0)),
"z": float(position.get("z", 0)),
}
elif isinstance(position, (list, tuple)) and len(position) >= 2:
return {
"x": float(position[0]),
"y": float(position[1]),
"z": float(position[2]) if len(position) > 2 else 0.0,
}
else:
return {"x": 0.0, "y": 0.0, "z": 0.0}
def _build_resource_mappings(self, deck: Deck):
"""递归构建资源映射"""
@@ -191,12 +284,12 @@ class WorkstationBase(ABC):
def set_hardware_interface(self, hardware_interface: Union[Any, str]):
"""设置硬件接口"""
self.hardware_interface = hardware_interface
logger.info(f"工作站 {self._ros_node.device_id} 硬件接口设置: {type(hardware_interface).__name__}")
logger.info(f"工作站 {self.device_id} 硬件接口设置: {type(hardware_interface).__name__}")
def set_workstation_node(self, workstation_node: "ROS2WorkstationNode"):
"""设置协议节点引用(用于代理模式)"""
self._ros_node = workstation_node
logger.info(f"工作站 {self._ros_node.device_id} 关联协议节点")
logger.info(f"工作站 {self.device_id} 关联协议节点")
# ============ 设备操作接口 ============
@@ -255,21 +348,21 @@ class WorkstationBase(ABC):
"""按类型查找资源"""
return [res for res in self.plr_resources.values() if isinstance(res, resource_type)]
def sync_with_external_system(self) -> bool:
async def sync_with_external_system(self) -> bool:
"""与外部物料系统同步"""
if not self.resource_synchronizer:
logger.info(f"工作站 {self._ros_node.device_id} 没有配置资源同步器")
logger.info(f"工作站 {self.device_id} 没有配置资源同步器")
return True
try:
success = self.resource_synchronizer.sync_from_external()
success = await self.resource_synchronizer.sync_from_external()
if success:
logger.info(f"工作站 {self._ros_node.device_id} 外部同步成功")
logger.info(f"工作站 {self.device_id} 外部同步成功")
else:
logger.warning(f"工作站 {self._ros_node.device_id} 外部同步失败")
logger.warning(f"工作站 {self.device_id} 外部同步失败")
return success
except Exception as e:
logger.error(f"工作站 {self._ros_node.device_id} 外部同步异常: {e}")
logger.error(f"工作站 {self.device_id} 外部同步异常: {e}")
return False
# ============ 简化的工作流控制 ============
@@ -287,23 +380,23 @@ class WorkstationBase(ABC):
if success:
self.current_workflow_status = WorkflowStatus.RUNNING
logger.info(f"工作站 {self._ros_node.device_id} 工作流 {workflow_name} 启动成功")
logger.info(f"工作站 {self.device_id} 工作流 {workflow_name} 启动成功")
else:
self.current_workflow_status = WorkflowStatus.ERROR
logger.error(f"工作站 {self._ros_node.device_id} 工作流 {workflow_name} 启动失败")
logger.error(f"工作站 {self.device_id} 工作流 {workflow_name} 启动失败")
return success
except Exception as e:
self.current_workflow_status = WorkflowStatus.ERROR
logger.error(f"工作站 {self._ros_node.device_id} 执行工作流失败: {e}")
logger.error(f"工作站 {self.device_id} 执行工作流失败: {e}")
return False
def stop_workflow(self, emergency: bool = False) -> bool:
"""停止工作流"""
try:
if self.current_workflow_status in [WorkflowStatus.IDLE, WorkflowStatus.STOPPED]:
logger.warning(f"工作站 {self._ros_node.device_id} 没有正在运行的工作流")
logger.warning(f"工作站 {self.device_id} 没有正在运行的工作流")
return True
self.current_workflow_status = WorkflowStatus.STOPPING
@@ -313,16 +406,16 @@ class WorkstationBase(ABC):
if success:
self.current_workflow_status = WorkflowStatus.STOPPED
logger.info(f"工作站 {self._ros_node.device_id} 工作流停止成功 (紧急: {emergency})")
logger.info(f"工作站 {self.device_id} 工作流停止成功 (紧急: {emergency})")
else:
self.current_workflow_status = WorkflowStatus.ERROR
logger.error(f"工作站 {self._ros_node.device_id} 工作流停止失败")
logger.error(f"工作站 {self.device_id} 工作流停止失败")
return success
except Exception as e:
self.current_workflow_status = WorkflowStatus.ERROR
logger.error(f"工作站 {self._ros_node.device_id} 停止工作流失败: {e}")
logger.error(f"工作站 {self.device_id} 停止工作流失败: {e}")
return False
# ============ 状态属性 ============
@@ -348,7 +441,49 @@ class WorkstationBase(ABC):
return 0.0
return time.time() - self.workflow_start_time
# ============ 抽象方法 - 子类必须实现 ============
class ProtocolNode(WorkstationBase):
def __init__(self, deck: Optional[PLRResource], *args, **kwargs):
super().__init__(deck, *args, **kwargs)
# @abstractmethod
# def _register_supported_workflows(self):
# """注册支持的工作流 - 子类必须实现"""
# pass
# @abstractmethod
# def _execute_workflow_impl(self, workflow_name: str, parameters: Dict[str, Any]) -> bool:
# """执行工作流的具体实现 - 子类必须实现"""
# pass
# @abstractmethod
# def _stop_workflow_impl(self, emergency: bool = False) -> bool:
# """停止工作流的具体实现 - 子类必须实现"""
# pass
class WorkstationExample(WorkstationBase):
"""工作站示例实现"""
def _register_supported_workflows(self):
"""注册支持的工作流"""
self.supported_workflows["example_workflow"] = WorkflowInfo(
name="example_workflow",
description="这是一个示例工作流",
estimated_duration=300.0,
required_materials=["sample_plate"],
output_product="processed_plate",
parameters_schema={"param1": "string", "param2": "integer"},
)
def _execute_workflow_impl(self, workflow_name: str, parameters: Dict[str, Any]) -> bool:
"""执行工作流的具体实现"""
if workflow_name not in self.supported_workflows:
logger.error(f"工作站 {self.device_id} 不支持工作流: {workflow_name}")
return False
# 这里添加实际的工作流逻辑
logger.info(f"工作站 {self.device_id} 正在执行工作流: {workflow_name} with parameters {parameters}")
return True
def _stop_workflow_impl(self, emergency: bool = False) -> bool:
"""停止工作流的具体实现"""
# 这里添加实际的停止逻辑
logger.info(f"工作站 {self.device_id} 正在停止工作流 (紧急: {emergency})")
return True

View File

@@ -0,0 +1,812 @@
bioyondworkstation_device:
category:
- bioyond_workstation
class:
action_value_mappings:
auto-Bioystation_1_to_2_task:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: Bioystation_1_to_2_task参数
type: object
type: UniLabJsonCommand
auto-Bioystation_3_to_2_task:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: Bioystation_3_to_2_task参数
type: object
type: UniLabJsonCommand
auto-Bioystation_feeding4to3_from_xlsx_task:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: Bioystation_feeding4to3_from_xlsx_task参数
type: object
type: UniLabJsonCommand
auto-Bioystation_scheduler_continue_task:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: Bioystation_scheduler_continue_task参数
type: object
type: UniLabJsonCommand
auto-Bioystation_scheduler_start_task:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: Bioystation_scheduler_start_task参数
type: object
type: UniLabJsonCommand
auto-Bioystation_scheduler_stop_task:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: Bioystation_scheduler_stop_task参数
type: object
type: UniLabJsonCommand
auto-Bioystation_start_experiment_task:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: Bioystation_start_experiment_task参数
type: object
type: UniLabJsonCommand
auto-auto_batch_outbound_from_xlsx:
feedback: {}
goal: {}
goal_default:
xlsx_path: null
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
xlsx_path:
type: string
required:
- xlsx_path
type: object
result: {}
required:
- goal
title: auto_batch_outbound_from_xlsx参数
type: object
type: UniLabJsonCommand
auto-auto_feeding4to3_from_xlsx:
feedback: {}
goal: {}
goal_default:
xlsx_path: null
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
xlsx_path:
type: string
required:
- xlsx_path
type: object
result: {}
required:
- goal
title: auto_feeding4to3_from_xlsx参数
type: object
type: UniLabJsonCommand
auto-create_orders:
feedback: {}
goal: {}
goal_default:
xlsx_path: null
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
xlsx_path:
type: string
required:
- xlsx_path
type: object
result: {}
required:
- goal
title: create_orders参数
type: object
type: UniLabJsonCommand
auto-order_list:
feedback: {}
goal: {}
goal_default:
begin_time: null
end_time: null
filter_text: null
page: 10
skip: 0
status: null
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
begin_time:
type: string
end_time:
type: string
filter_text:
type: string
page:
default: 10
type: integer
skip:
default: 0
type: integer
status:
type: string
required: []
type: object
result: {}
required:
- goal
title: order_list参数
type: object
type: UniLabJsonCommand
auto-order_list_v2:
feedback: {}
goal: {}
goal_default:
beginTime: ''
endTime: ''
filter: ''
pageCount: 1
skipCount: 0
sorting: ''
status: ''
timeType: string
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
beginTime:
default: ''
type: string
endTime:
default: ''
type: string
filter:
default: ''
type: string
pageCount:
default: 1
type: integer
skipCount:
default: 0
type: integer
sorting:
default: ''
type: string
status:
default: ''
type: string
timeType:
default: string
type: string
required: []
type: object
result: {}
required:
- goal
title: order_list_v2参数
type: object
type: UniLabJsonCommand
auto-order_report:
feedback: {}
goal: {}
goal_default:
order_id: null
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
order_id:
type: string
required:
- order_id
type: object
result: {}
required:
- goal
title: order_report参数
type: object
type: UniLabJsonCommand
auto-report_material_change:
feedback: {}
goal: {}
goal_default:
material_obj: null
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
material_obj:
type: object
required:
- material_obj
type: object
result: {}
required:
- goal
title: report_material_change参数
type: object
type: UniLabJsonCommand
auto-report_order_finish:
feedback: {}
goal: {}
goal_default:
completion_time: null
end_time: null
order_code: null
order_name: null
start_time: null
status: '30'
used_materials: null
workflow_status: Finished
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
completion_time:
type: string
end_time:
type: string
order_code:
type: string
order_name:
type: string
start_time:
type: string
status:
default: '30'
type: string
used_materials:
type: string
workflow_status:
default: Finished
type: string
required:
- order_code
- order_name
- start_time
- end_time
type: object
result: {}
required:
- goal
title: report_order_finish参数
type: object
type: UniLabJsonCommand
auto-report_step_finish:
feedback: {}
goal: {}
goal_default:
end_time: null
execution_status: completed
order_code: null
order_name: null
sample_id: null
start_time: null
step_id: null
step_name: null
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
end_time:
type: string
execution_status:
default: completed
type: string
order_code:
type: string
order_name:
type: string
sample_id:
type: string
start_time:
type: string
step_id:
type: string
step_name:
type: string
required:
- order_code
- order_name
- step_name
- step_id
- sample_id
- start_time
- end_time
type: object
result: {}
required:
- goal
title: report_step_finish参数
type: object
type: UniLabJsonCommand
auto-run_full_workflow:
feedback: {}
goal: {}
goal_default:
inbound_items: null
orders: null
poll_filter_code: null
poll_interval_s: 5
poll_timeout_s: 600
takeout_order_id: null
transfer_source: null
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
inbound_items:
items:
type: object
type: array
orders:
items:
type: object
type: array
poll_filter_code:
type: string
poll_interval_s:
default: 5
type: integer
poll_timeout_s:
default: 600
type: integer
takeout_order_id:
type: string
transfer_source:
type: string
required:
- inbound_items
- orders
type: object
result: {}
required:
- goal
title: run_full_workflow参数
type: object
type: UniLabJsonCommand
auto-scheduler_continue:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: scheduler_continue参数
type: object
type: UniLabJsonCommand
auto-scheduler_start:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: scheduler_start参数
type: object
type: UniLabJsonCommand
auto-scheduler_stop:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: scheduler_stop参数
type: object
type: UniLabJsonCommand
auto-start_station1_internal_flow:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: start_station1_internal_flow参数
type: object
type: UniLabJsonCommand
auto-storage_batch_inbound:
feedback: {}
goal: {}
goal_default:
items: null
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
items:
items:
type: object
type: array
required:
- items
type: object
result: {}
required:
- goal
title: storage_batch_inbound参数
type: object
type: UniLabJsonCommand
auto-storage_inbound:
feedback: {}
goal: {}
goal_default:
location_id: null
material_id: null
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
location_id:
type: string
material_id:
type: string
required:
- material_id
- location_id
type: object
result: {}
required:
- goal
title: storage_inbound参数
type: object
type: UniLabJsonCommand
auto-take_out:
feedback: {}
goal: {}
goal_default:
material_ids: null
order_id: null
preintake_ids: null
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
material_ids:
type: string
order_id:
type: string
preintake_ids:
type: string
required:
- order_id
type: object
result: {}
required:
- goal
title: take_out参数
type: object
type: UniLabJsonCommand
auto-test_benyao_workstation:
feedback: {}
goal: {}
goal_default:
num1: null
num2: null
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
num1:
type: string
num2:
type: string
required:
- num1
- num2
type: object
result: {}
required:
- goal
title: test_benyao_workstation参数
type: object
type: UniLabJsonCommand
auto-transfer_1_to_2:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: transfer_1_to_2参数
type: object
type: UniLabJsonCommand
auto-transfer_3_to_2_to_1:
feedback: {}
goal: {}
goal_default:
source_wh_id: 3a19debc-84b4-0359-e2d4-b3beea49348b
source_x: 1
source_y: 1
source_z: 1
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
source_wh_id:
default: 3a19debc-84b4-0359-e2d4-b3beea49348b
type: string
source_x:
default: 1
type: integer
source_y:
default: 1
type: integer
source_z:
default: 1
type: integer
required: []
type: object
result: {}
required:
- goal
title: transfer_3_to_2_to_1参数
type: object
type: UniLabJsonCommand
auto-wait_for_transfer_task:
feedback: {}
goal: {}
goal_default:
filter_text: null
interval: 5
timeout: 3000
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
filter_text:
type: string
interval:
default: 5
type: integer
timeout:
default: 3000
type: integer
required: []
type: object
result: {}
required:
- goal
title: wait_for_transfer_task参数
type: object
type: UniLabJsonCommand
module: unilabos.devices.workstation.bioyond_cell.bioyond_workstation:BioyondWorkstation
status_types: {}
type: python
config_info: []
description: 宜宾配液分液工站
handles: []
icon: ''
init_param_schema:
config:
properties:
bioyond_config:
type: string
debug_mode:
default: false
type: boolean
station_resource:
type: string
required: []
type: object
data:
properties: {}
required: []
type: object
registry_type: device
version: 1.0.0

View File

@@ -0,0 +1,506 @@
dispensing_station.bioyond:
category:
- work_station
- dispensing_station_bioyond
class:
action_value_mappings:
bioyond_sync:
feedback: {}
goal:
force_sync: force_sync
sync_type: sync_type
goal_default:
force_sync: false
sync_type: full
handles: {}
result: {}
schema:
description: 从Bioyond系统同步物料
properties:
feedback: {}
goal:
properties:
force_sync:
description: 是否强制同步
type: boolean
sync_type:
description: 同步类型
enum:
- full
- incremental
type: string
required:
- sync_type
type: object
result: {}
required:
- goal
title: bioyond_sync参数
type: object
type: UniLabJsonCommand
bioyond_update:
feedback: {}
goal:
material_ids: material_ids
sync_all: sync_all
goal_default:
material_ids: []
sync_all: true
handles: {}
result: {}
schema:
description: 将本地物料变更同步到Bioyond
properties:
feedback: {}
goal:
properties:
material_ids:
description: 要同步的物料ID列表
items:
type: string
type: array
sync_all:
description: 是否同步所有物料
type: boolean
required:
- sync_all
type: object
result: {}
required:
- goal
title: bioyond_update参数
type: object
type: UniLabJsonCommand
create_90_10_vial_feeding_task:
feedback: {}
goal:
delay_time: delay_time
order_name: order_name
percent_10_1_assign_material_name: percent_10_1_assign_material_name
percent_10_1_liquid_material_name: percent_10_1_liquid_material_name
percent_10_1_target_weigh: percent_10_1_target_weigh
percent_10_1_volume: percent_10_1_volume
percent_10_2_assign_material_name: percent_10_2_assign_material_name
percent_10_2_liquid_material_name: percent_10_2_liquid_material_name
percent_10_2_target_weigh: percent_10_2_target_weigh
percent_10_2_volume: percent_10_2_volume
percent_90_1_assign_material_name: percent_90_1_assign_material_name
percent_90_1_target_weigh: percent_90_1_target_weigh
percent_90_2_assign_material_name: percent_90_2_assign_material_name
percent_90_2_target_weigh: percent_90_2_target_weigh
percent_90_3_assign_material_name: percent_90_3_assign_material_name
percent_90_3_target_weigh: percent_90_3_target_weigh
speed: speed
temperature: temperature
goal_default:
delay_time: '600'
order_name: ''
percent_10_1_assign_material_name: ''
percent_10_1_liquid_material_name: ''
percent_10_1_target_weigh: ''
percent_10_1_volume: ''
percent_10_2_assign_material_name: ''
percent_10_2_liquid_material_name: ''
percent_10_2_target_weigh: ''
percent_10_2_volume: ''
percent_90_1_assign_material_name: ''
percent_90_1_target_weigh: ''
percent_90_2_assign_material_name: ''
percent_90_2_target_weigh: ''
percent_90_3_assign_material_name: ''
percent_90_3_target_weigh: ''
speed: '400'
temperature: '20'
handles: {}
result: {}
schema:
description: 创建90%/10%小瓶投料任务
properties:
feedback: {}
goal:
properties:
delay_time:
default: '600'
description: 延迟时间(s)
type: string
order_name:
description: 任务名称
type: string
percent_10_1_assign_material_name:
description: 10%组分1物料名称
type: string
percent_10_1_liquid_material_name:
description: 10%组分1液体物料名称
type: string
percent_10_1_target_weigh:
description: 10%组分1目标重量(g)
type: string
percent_10_1_volume:
description: 10%组分1液体体积(mL)
type: string
percent_10_2_assign_material_name:
description: 10%组分2物料名称
type: string
percent_10_2_liquid_material_name:
description: 10%组分2液体物料名称
type: string
percent_10_2_target_weigh:
description: 10%组分2目标重量(g)
type: string
percent_10_2_volume:
description: 10%组分2液体体积(mL)
type: string
percent_90_1_assign_material_name:
description: 90%组分1物料名称
type: string
percent_90_1_target_weigh:
description: 90%组分1目标重量(g)
type: string
percent_90_2_assign_material_name:
description: 90%组分2物料名称
type: string
percent_90_2_target_weigh:
description: 90%组分2目标重量(g)
type: string
percent_90_3_assign_material_name:
description: 90%组分3物料名称
type: string
percent_90_3_target_weigh:
description: 90%组分3目标重量(g)
type: string
speed:
default: '400'
description: 搅拌速度(rpm)
type: string
temperature:
default: '20'
description: 温度(°C)
type: string
type: object
result: {}
required:
- goal
title: create_90_10_vial_feeding_task参数
type: object
type: UniLabJsonCommand
create_batch_90_10_vial_feeding_task:
feedback: {}
goal:
batch_data: batch_data
goal_default:
batch_data: '{}'
handles: {}
result: {}
schema:
description: 创建批量90%10%小瓶投料任务
properties:
feedback: {}
goal:
properties:
batch_data:
description: 批量90%10%小瓶投料任务数据(JSON格式)包含batch_name、tasks列表和global_settings
type: string
required:
- batch_data
type: object
result: {}
required:
- goal
title: create_batch_90_10_vial_feeding_task参数
type: object
type: UniLabJsonCommand
create_batch_diamine_solution_task:
feedback: {}
goal:
batch_data: batch_data
goal_default:
batch_data: '{}'
handles: {}
result: {}
schema:
description: 创建批量二胺溶液配制任务
properties:
feedback: {}
goal:
properties:
batch_data:
description: 批量二胺溶液配制任务数据(JSON格式)包含batch_name、tasks列表和global_settings
type: string
required:
- batch_data
type: object
result: {}
required:
- goal
title: create_batch_diamine_solution_task参数
type: object
type: UniLabJsonCommand
create_diamine_solution_task:
feedback: {}
goal:
delay_time: delay_time
hold_m_name: hold_m_name
liquid_material_name: liquid_material_name
material_name: material_name
order_name: order_name
speed: speed
target_weigh: target_weigh
temperature: temperature
volume: volume
goal_default:
delay_time: '600'
hold_m_name: ''
liquid_material_name: NMP
material_name: ''
order_name: ''
speed: '400'
target_weigh: ''
temperature: '20'
volume: ''
handles: {}
result: {}
schema:
description: 创建二胺溶液配制任务
properties:
feedback: {}
goal:
properties:
delay_time:
default: '600'
description: 延迟时间(s)
type: string
hold_m_name:
description: 库位名称(如ODA-1)
type: string
liquid_material_name:
default: NMP
description: 液体物料名称
type: string
material_name:
description: 固体物料名称
type: string
order_name:
description: 任务名称
type: string
speed:
default: '400'
description: 搅拌速度(rpm)
type: string
target_weigh:
description: 固体目标重量(g)
type: string
temperature:
default: '20'
description: 温度(°C)
type: string
volume:
description: 液体体积(mL)
type: string
required:
- material_name
- target_weigh
- volume
type: object
result: {}
required:
- goal
title: create_diamine_solution_task参数
type: object
type: UniLabJsonCommand
create_resource:
feedback: {}
goal:
resource_config: resource_config
resource_type: resource_type
goal_default:
resource_config: {}
resource_type: ''
handles: {}
result: {}
schema:
description: 创建资源操作
properties:
feedback: {}
goal:
properties:
resource_config:
description: 资源配置
type: object
resource_type:
description: 资源类型
type: string
required:
- resource_type
- resource_config
type: object
result: {}
required:
- goal
title: create_resource参数
type: object
type: UniLabJsonCommand
dispensing_material_inbound:
feedback: {}
goal:
location: location
material_id: material_id
goal_default:
location: ''
material_id: ''
handles: {}
result: {}
schema:
description: 配液站物料入库操作
properties:
feedback: {}
goal:
properties:
location:
description: 存储位置
type: string
material_id:
description: 物料ID
type: string
required:
- material_id
- location
type: object
result: {}
required:
- goal
title: dispensing_material_inbound参数
type: object
type: UniLabJsonCommand
dispensing_material_outbound:
feedback: {}
goal:
material_id: material_id
quantity: quantity
goal_default:
material_id: ''
quantity: 0.0
handles: {}
result: {}
schema:
description: 配液站物料出库操作
properties:
feedback: {}
goal:
properties:
material_id:
description: 物料ID
type: string
quantity:
description: 出库数量
type: number
required:
- material_id
- quantity
type: object
result: {}
required:
- goal
title: dispensing_material_outbound参数
type: object
type: UniLabJsonCommand
sample_waste_removal:
feedback: {}
goal:
sample_id: sample_id
waste_type: waste_type
goal_default:
sample_id: ''
waste_type: general
handles: {}
result: {}
schema:
description: 样品废料移除操作
properties:
feedback: {}
goal:
properties:
sample_id:
description: 样品ID
type: string
waste_type:
description: 废料类型
enum:
- general
- hazardous
- organic
- inorganic
type: string
required:
- sample_id
type: object
result: {}
required:
- goal
title: sample_waste_removal参数
type: object
type: UniLabJsonCommand
module: unilabos.devices.workstation.bioyond_studio.station:BioyondWorkstation
protocol_type: []
status_types:
bioyond_status: dict
enable_dispensing_station: bool
enable_reaction_station: bool
station_type: str
type: python
config_info: []
description: Bioyond配液站 - 专门用于物料配制和管理的工作站
handles: []
icon: 配液站.webp
init_param_schema:
config:
properties:
bioyond_config:
description: Bioyond API配置
properties:
api_host:
description: Bioyond API主机地址
type: string
api_key:
description: Bioyond API密钥
type: string
material_type_mappings:
description: 物料类型映射配置
type: object
workflow_mappings:
description: 工作流映射配置
type: object
type: object
deck:
description: Deck配置
type: object
station_config:
description: 配液站配置
properties:
description:
description: 配液站描述
type: string
enable_dispensing_station:
default: true
description: 启用配液站功能
type: boolean
enable_reaction_station:
default: false
description: 禁用反应站功能
type: boolean
station_name:
description: 配液站名称
type: string
station_type:
default: dispensing_station
description: 站点类型 - 配液站
enum:
- dispensing_station
type: string
type: object
required: []
type: object
data:
properties: {}
required: []
type: object
version: 1.0.0

View File

@@ -0,0 +1,384 @@
reaction_station.bioyond:
category:
- work_station
- reaction_station_bioyond
class:
action_value_mappings:
bioyond_sync:
feedback: {}
goal:
force_sync: force_sync
sync_type: sync_type
goal_default:
force_sync: false
sync_type: full
handles: {}
result: {}
schema:
description: 从Bioyond系统同步物料
properties:
feedback: {}
goal:
properties:
force_sync:
description: 是否强制同步
type: boolean
sync_type:
description: 同步类型
enum:
- full
- incremental
type: string
required:
- sync_type
type: object
result: {}
required:
- goal
title: bioyond_sync参数
type: object
type: UniLabJsonCommand
bioyond_update:
feedback: {}
goal:
material_ids: material_ids
sync_all: sync_all
goal_default:
material_ids: []
sync_all: true
handles: {}
result: {}
schema:
description: 将本地物料变更同步到Bioyond
properties:
feedback: {}
goal:
properties:
material_ids:
description: 要同步的物料ID列表
items:
type: string
type: array
sync_all:
description: 是否同步所有物料
type: boolean
required:
- sync_all
type: object
result: {}
required:
- goal
title: bioyond_update参数
type: object
type: UniLabJsonCommand
reaction_station_drip_back:
feedback: {}
goal:
assign_material_name: assign_material_name
time: time
torque_variation: torque_variation
volume: volume
goal_default:
assign_material_name: ''
time: ''
torque_variation: ''
volume: ''
handles: {}
result: {}
schema:
description: 反应站滴回操作
properties:
feedback: {}
goal:
properties:
assign_material_name:
description: 溶剂名称
type: string
time:
description: 观察时间单位min
type: string
torque_variation:
description: 是否观察1否2是
type: string
volume:
description: 投料体积
type: string
required:
- volume
- assign_material_name
- time
- torque_variation
type: object
result: {}
required:
- goal
title: reaction_station_drip_back参数
type: object
type: UniLabJsonCommand
reaction_station_liquid_feed:
feedback: {}
goal:
assign_material_name: assign_material_name
time: time
titration_type: titration_type
torque_variation: torque_variation
volume: volume
goal_default:
assign_material_name: ''
time: ''
titration_type: ''
torque_variation: ''
volume: ''
handles: {}
result: {}
schema:
description: 反应站液体进料操作
properties:
feedback: {}
goal:
properties:
assign_material_name:
description: 溶剂名称
type: string
time:
description: 观察时间单位min
type: string
titration_type:
description: 滴定类型1否2是
type: string
torque_variation:
description: 是否观察1否2是
type: string
volume:
description: 投料体积
type: string
required:
- titration_type
- volume
- assign_material_name
- time
- torque_variation
type: object
result: {}
required:
- goal
title: reaction_station_liquid_feed参数
type: object
type: UniLabJsonCommand
reaction_station_process_execute:
feedback: {}
goal:
task_name: task_name
workflow_name: workflow_name
goal_default:
task_name: ''
workflow_name: ''
handles: {}
result: {}
schema:
description: 反应站流程执行
properties:
feedback: {}
goal:
properties:
task_name:
description: 任务名称
type: string
workflow_name:
description: 工作流名称
type: string
required:
- workflow_name
- task_name
type: object
result: {}
required:
- goal
title: reaction_station_process_execute参数
type: object
type: UniLabJsonCommand
reaction_station_reactor_taken_out:
feedback: {}
goal:
order_id: order_id
preintake_id: preintake_id
goal_default:
order_id: ''
preintake_id: ''
handles: {}
result: {}
schema:
description: 反应站反应器取出操作 - 通过订单ID和预取样ID进行精确控制
properties:
feedback: {}
goal:
properties:
order_id:
description: 订单ID用于标识要取出的订单
type: string
preintake_id:
description: 预取样ID用于标识具体的取样任务
type: string
required: []
type: object
result:
properties:
code:
description: 操作结果代码1表示成功0表示失败
type: integer
return_info:
description: 操作结果详细信息
type: string
type: object
required:
- goal
title: reaction_station_reactor_taken_out参数
type: object
type: UniLabJsonCommand
reaction_station_solid_feed_vial:
feedback: {}
goal:
assign_material_name: assign_material_name
material_id: material_id
time: time
torque_variation: torque_variation
goal_default:
assign_material_name: ''
material_id: ''
time: ''
torque_variation: ''
handles: {}
result: {}
schema:
description: 反应站固体进料操作
properties:
feedback: {}
goal:
properties:
assign_material_name:
description: 固体名称_粉末加样模块-投料
type: string
material_id:
description: 固体投料类型_粉末加样模块-投料
type: string
time:
description: 观察时间_反应模块-观察搅拌结果
type: string
torque_variation:
description: 是否观察1否2是_反应模块-观察搅拌结果
type: string
required:
- assign_material_name
- material_id
- time
- torque_variation
type: object
result: {}
required:
- goal
title: reaction_station_solid_feed_vial参数
type: object
type: UniLabJsonCommand
reaction_station_take_in:
feedback: {}
goal:
assign_material_name: assign_material_name
cutoff: cutoff
temperature: temperature
goal_default:
assign_material_name: ''
cutoff: ''
temperature: ''
handles: {}
result: {}
schema:
description: 反应站取入操作
properties:
feedback: {}
goal:
properties:
assign_material_name:
description: 物料名称
type: string
cutoff:
description: 截止参数
type: string
temperature:
description: 温度
type: string
required:
- cutoff
- temperature
- assign_material_name
type: object
result: {}
required:
- goal
title: reaction_station_take_in参数
type: object
type: UniLabJsonCommand
module: unilabos.devices.workstation.bioyond_studio.station:BioyondWorkstation
protocol_type: []
status_types:
bioyond_status: dict
enable_dispensing_station: bool
enable_reaction_station: bool
station_type: str
type: python
config_info: []
description: Bioyond反应站 - 专门用于化学反应操作的工作站
handles: []
icon: 反应站.webp
init_param_schema:
config:
properties:
bioyond_config:
description: Bioyond API配置
properties:
api_host:
description: Bioyond API主机地址
type: string
api_key:
description: Bioyond API密钥
type: string
material_type_mappings:
description: 物料类型映射配置
type: object
workflow_mappings:
description: 工作流映射配置
type: object
type: object
deck:
description: Deck配置
type: object
station_config:
description: 反应站配置
properties:
description:
description: 反应站描述
type: string
enable_dispensing_station:
default: false
description: 禁用配液站功能
type: boolean
enable_reaction_station:
default: true
description: 启用反应站功能
type: boolean
station_name:
description: 反应站名称
type: string
station_type:
default: reaction_station
description: 站点类型 - 反应站
enum:
- reaction_station
type: string
type: object
required: []
type: object
data:
properties: {}
required: []
type: object
version: 1.0.0

View File

@@ -1,481 +0,0 @@
reaction_station_bioyong:
category:
- reaction_station_bioyong
class:
action_value_mappings:
drip_back:
feedback: {}
goal:
assign_material_name: assign_material_name
time: time
torque_variation: torque_variation
volume: volume
goal_default:
assign_material_name: ''
time: ''
torque_variation: ''
volume: ''
handles: {}
result:
return_info: return_info
schema:
description: ''
properties:
feedback:
properties: {}
required: []
title: ReactionStationDripBack_Feedback
type: object
goal:
properties:
assign_material_name:
type: string
time:
type: string
torque_variation:
type: string
volume:
type: string
required:
- volume
- assign_material_name
- time
- torque_variation
title: ReactionStationDripBack_Goal
type: object
result:
properties:
return_info:
type: string
required:
- return_info
title: ReactionStationDripBack_Result
type: object
required:
- goal
title: ReactionStationDripBack
type: object
type: ReactionStationDripBack
liquid_feeding_beaker:
feedback: {}
goal:
assign_material_name: assign_material_name
time: time
torque_variation: torque_variation
volume: volume
goal_default:
assign_material_name: ''
time: ''
titration_type: ''
torque_variation: ''
volume: ''
handles: {}
result:
return_info: return_info
schema:
description: ''
properties:
feedback:
properties: {}
required: []
title: ReactionStationLiquidFeed_Feedback
type: object
goal:
properties:
assign_material_name:
type: string
time:
type: string
titration_type:
type: string
torque_variation:
type: string
volume:
type: string
required:
- titration_type
- volume
- assign_material_name
- time
- torque_variation
title: ReactionStationLiquidFeed_Goal
type: object
result:
properties:
return_info:
type: string
required:
- return_info
title: ReactionStationLiquidFeed_Result
type: object
required:
- goal
title: ReactionStationLiquidFeed
type: object
type: ReactionStationLiquidFeed
liquid_feeding_solvents:
feedback: {}
goal:
assign_material_name: assign_material_name
time: time
torque_variation: torque_variation
volume: volume
goal_default:
assign_material_name: ''
time: ''
titration_type: ''
torque_variation: ''
volume: ''
handles: {}
result:
return_info: return_info
schema:
description: ''
properties:
feedback:
properties: {}
required: []
title: ReactionStationLiquidFeed_Feedback
type: object
goal:
properties:
assign_material_name:
type: string
time:
type: string
titration_type:
type: string
torque_variation:
type: string
volume:
type: string
required:
- titration_type
- volume
- assign_material_name
- time
- torque_variation
title: ReactionStationLiquidFeed_Goal
type: object
result:
properties:
return_info:
type: string
required:
- return_info
title: ReactionStationLiquidFeed_Result
type: object
required:
- goal
title: ReactionStationLiquidFeed
type: object
type: ReactionStationLiquidFeed
liquid_feeding_titration:
feedback: {}
goal:
assign_material_name: assign_material_name
time: time
torque_variation: torque_variation
volume: volume
goal_default:
assign_material_name: ''
time: ''
titration_type: ''
torque_variation: ''
volume: ''
handles: {}
result:
return_info: return_info
schema:
description: ''
properties:
feedback:
properties: {}
required: []
title: ReactionStationLiquidFeed_Feedback
type: object
goal:
properties:
assign_material_name:
type: string
time:
type: string
titration_type:
type: string
torque_variation:
type: string
volume:
type: string
required:
- titration_type
- volume
- assign_material_name
- time
- torque_variation
title: ReactionStationLiquidFeed_Goal
type: object
result:
properties:
return_info:
type: string
required:
- return_info
title: ReactionStationLiquidFeed_Result
type: object
required:
- goal
title: ReactionStationLiquidFeed
type: object
type: ReactionStationLiquidFeed
liquid_feeding_vials_non_titration:
feedback: {}
goal:
assign_material_name: assign_material_name
time: time
torque_variation: torque_variation
volume: volume
goal_default:
assign_material_name: ''
time: ''
titration_type: ''
torque_variation: ''
volume: ''
handles: {}
result:
return_info: return_info
schema:
description: ''
properties:
feedback:
properties: {}
required: []
title: ReactionStationLiquidFeed_Feedback
type: object
goal:
properties:
assign_material_name:
type: string
time:
type: string
titration_type:
type: string
torque_variation:
type: string
volume:
type: string
required:
- titration_type
- volume
- assign_material_name
- time
- torque_variation
title: ReactionStationLiquidFeed_Goal
type: object
result:
properties:
return_info:
type: string
required:
- return_info
title: ReactionStationLiquidFeed_Result
type: object
required:
- goal
title: ReactionStationLiquidFeed
type: object
type: ReactionStationLiquidFeed
process_and_execute_workflow:
feedback: {}
goal:
task_name: task_name
workflow_name: workflow_name
goal_default:
task_name: ''
workflow_name: ''
handles: {}
result:
return_info: return_info
schema:
description: ''
properties:
feedback:
properties: {}
required: []
title: ReactionStationProExecu_Feedback
type: object
goal:
properties:
task_name:
type: string
workflow_name:
type: string
required:
- workflow_name
- task_name
title: ReactionStationProExecu_Goal
type: object
result:
properties:
return_info:
type: string
required:
- return_info
title: ReactionStationProExecu_Result
type: object
required:
- goal
title: ReactionStationProExecu
type: object
type: ReactionStationProExecu
reactor_taken_in:
feedback: {}
goal:
assign_material_name: assign_material_name
cutoff: cutoff
temperature: temperature
goal_default:
assign_material_name: ''
cutoff: ''
temperature: ''
handles: {}
result:
return_info: return_info
schema:
description: ''
properties:
feedback:
properties: {}
required: []
title: ReactionStationReaTackIn_Feedback
type: object
goal:
properties:
assign_material_name:
type: string
cutoff:
type: string
temperature:
type: string
required:
- cutoff
- temperature
- assign_material_name
title: ReactionStationReaTackIn_Goal
type: object
result:
properties:
return_info:
type: string
required:
- return_info
title: ReactionStationReaTackIn_Result
type: object
required:
- goal
title: ReactionStationReaTackIn
type: object
type: ReactionStationReaTackIn
reactor_taken_out:
feedback: {}
goal: {}
goal_default:
command: ''
handles: {}
result: {}
schema:
description: ''
properties:
feedback:
properties:
status:
type: string
required:
- status
title: SendCmd_Feedback
type: object
goal:
properties:
command:
type: string
required:
- command
title: SendCmd_Goal
type: object
result:
properties:
return_info:
type: string
success:
type: boolean
required:
- return_info
- success
title: SendCmd_Result
type: object
required:
- goal
title: SendCmd
type: object
type: SendCmd
solid_feeding_vials:
feedback: {}
goal:
assign_material_name: assign_material_name
material_id: material_id
time: time
torque_variation: torque_variation
goal_default:
assign_material_name: ''
material_id: ''
time: ''
torque_variation: ''
handles: {}
result:
return_info: return_info
schema:
description: ''
properties:
feedback:
properties: {}
required: []
title: ReactionStationSolidFeedVial_Feedback
type: object
goal:
properties:
assign_material_name:
type: string
material_id:
type: string
time:
type: string
torque_variation:
type: string
required:
- assign_material_name
- material_id
- time
- torque_variation
title: ReactionStationSolidFeedVial_Goal
type: object
result:
properties:
return_info:
type: string
required:
- return_info
title: ReactionStationSolidFeedVial_Result
type: object
required:
- goal
title: ReactionStationSolidFeedVial
type: object
type: ReactionStationSolidFeedVial
module: unilabos.devices.reaction_station.reaction_station_bioyong:BioyongV1RPC
status_types: {}
type: python
config_info: []
description: reaction_station_bioyong Device
handles: []
icon: ''
init_param_schema: {}
version: 1.0.0

View File

@@ -1,3 +1,517 @@
bettery_station_registry:
category:
- work_station
class:
action_value_mappings:
auto-change_hole_sheet_to_2:
feedback: {}
goal: {}
goal_default:
hole: null
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
hole:
type: object
required:
- hole
type: object
result: {}
required:
- goal
title: change_hole_sheet_to_2参数
type: object
type: UniLabJsonCommandAsync
auto-fill_plate:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: fill_plate参数
type: object
type: UniLabJsonCommandAsync
auto-fun_wuliao_test:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: fun_wuliao_test参数
type: object
type: UniLabJsonCommand
auto-func_allpack_cmd:
feedback: {}
goal: {}
goal_default:
assembly_pressure: 4200
assembly_type: 7
elec_num: null
elec_use_num: null
elec_vol: 50
file_path: D:\coin_cell_data
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
assembly_pressure:
default: 4200
type: integer
assembly_type:
default: 7
type: integer
elec_num:
type: string
elec_use_num:
type: string
elec_vol:
default: 50
type: integer
file_path:
default: D:\coin_cell_data
type: string
required:
- elec_num
- elec_use_num
type: object
result: {}
required:
- goal
title: func_allpack_cmd参数
type: object
type: UniLabJsonCommand
auto-func_get_csv_export_status:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: func_get_csv_export_status参数
type: object
type: UniLabJsonCommand
auto-func_pack_device_auto:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: func_pack_device_auto参数
type: object
type: UniLabJsonCommand
auto-func_pack_device_init:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: func_pack_device_init参数
type: object
type: UniLabJsonCommand
auto-func_pack_device_start:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: func_pack_device_start参数
type: object
type: UniLabJsonCommand
auto-func_pack_device_stop:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: func_pack_device_stop参数
type: object
type: UniLabJsonCommand
auto-func_pack_get_msg_cmd:
feedback: {}
goal: {}
goal_default:
file_path: D:\coin_cell_data
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
file_path:
default: D:\coin_cell_data
type: string
required: []
type: object
result: {}
required:
- goal
title: func_pack_get_msg_cmd参数
type: object
type: UniLabJsonCommand
auto-func_pack_send_bottle_num:
feedback: {}
goal: {}
goal_default:
bottle_num: null
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
bottle_num:
type: string
required:
- bottle_num
type: object
result: {}
required:
- goal
title: func_pack_send_bottle_num参数
type: object
type: UniLabJsonCommand
auto-func_pack_send_finished_cmd:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: func_pack_send_finished_cmd参数
type: object
type: UniLabJsonCommand
auto-func_pack_send_msg_cmd:
feedback: {}
goal: {}
goal_default:
assembly_pressure: null
assembly_type: null
elec_use_num: null
elec_vol: null
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
assembly_pressure:
type: string
assembly_type:
type: string
elec_use_num:
type: string
elec_vol:
type: string
required:
- elec_use_num
- elec_vol
- assembly_type
- assembly_pressure
type: object
result: {}
required:
- goal
title: func_pack_send_msg_cmd参数
type: object
type: UniLabJsonCommand
auto-func_read_data_and_output:
feedback: {}
goal: {}
goal_default:
file_path: D:\coin_cell_data
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
file_path:
default: D:\coin_cell_data
type: string
required: []
type: object
result: {}
required:
- goal
title: func_read_data_and_output参数
type: object
type: UniLabJsonCommand
auto-func_stop_read_data:
feedback: {}
goal: {}
goal_default: {}
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: func_stop_read_data参数
type: object
type: UniLabJsonCommand
auto-modify_deck_name:
feedback: {}
goal: {}
goal_default:
resource_name: null
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
resource_name:
type: string
required:
- resource_name
type: object
result: {}
required:
- goal
title: modify_deck_name参数
type: object
type: UniLabJsonCommand
auto-post_init:
feedback: {}
goal: {}
goal_default:
ros_node: null
handles: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
ros_node:
type: object
required:
- ros_node
type: object
result: {}
required:
- goal
title: post_init参数
type: object
type: UniLabJsonCommand
module: unilabos.devices.workstation.coin_cell_assembly.coin_cell_assembly:CoinCellAssemblyWorkstation
status_types:
data_assembly_coin_cell_num: int
data_assembly_pressure: int
data_assembly_time: float
data_axis_x_pos: float
data_axis_y_pos: float
data_axis_z_pos: float
data_coin_cell_code: str
data_coin_num: int
data_electrolyte_code: str
data_electrolyte_volume: int
data_glove_box_o2_content: float
data_glove_box_pressure: float
data_glove_box_water_content: float
data_open_circuit_voltage: float
data_pole_weight: float
request_rec_msg_status: bool
request_send_msg_status: bool
sys_mode: str
sys_status: str
type: python
config_info: []
description: ''
handles: []
icon: ''
init_param_schema:
config:
properties:
address:
default: 192.168.1.20
type: string
debug_mode:
default: true
type: boolean
port:
default: '502'
type: string
station_resource:
type: object
required:
- station_resource
type: object
data:
properties:
data_assembly_coin_cell_num:
type: integer
data_assembly_pressure:
type: integer
data_assembly_time:
type: number
data_axis_x_pos:
type: number
data_axis_y_pos:
type: number
data_axis_z_pos:
type: number
data_coin_cell_code:
type: string
data_coin_num:
type: integer
data_electrolyte_code:
type: string
data_electrolyte_volume:
type: integer
data_glove_box_o2_content:
type: number
data_glove_box_pressure:
type: number
data_glove_box_water_content:
type: number
data_open_circuit_voltage:
type: number
data_pole_weight:
type: number
request_rec_msg_status:
type: boolean
request_send_msg_status:
type: boolean
sys_mode:
type: string
sys_status:
type: string
required:
- sys_status
- sys_mode
- request_rec_msg_status
- request_send_msg_status
- data_assembly_coin_cell_num
- data_assembly_time
- data_open_circuit_voltage
- data_axis_x_pos
- data_axis_y_pos
- data_axis_z_pos
- data_pole_weight
- data_assembly_pressure
- data_electrolyte_volume
- data_coin_num
- data_coin_cell_code
- data_electrolyte_code
- data_glove_box_pressure
- data_glove_box_o2_content
- data_glove_box_water_content
type: object
version: 1.0.0
workstation:
category:
- work_station
@@ -6024,9 +6538,97 @@ workstation:
title: WashSolid
type: object
type: WashSolid
module: unilabos.devices.workstation.workstation_base:ProtocolNode
auto-create_ros_action_server:
feedback: {}
goal: {}
goal_default:
action_name: null
action_value_mapping: null
handles: {}
result: {}
schema:
description: create_ros_action_server的参数schema
properties:
feedback: {}
goal:
properties:
action_name:
type: string
action_value_mapping:
type: string
required:
- action_name
- action_value_mapping
type: object
result: {}
required:
- goal
title: create_ros_action_server参数
type: object
type: UniLabJsonCommand
auto-execute_single_action:
feedback: {}
goal: {}
goal_default:
action_kwargs: null
action_name: null
device_id: null
handles: {}
result: {}
schema:
description: execute_single_action的参数schema
properties:
feedback: {}
goal:
properties:
action_kwargs:
type: string
action_name:
type: string
device_id:
type: string
required:
- device_id
- action_name
- action_kwargs
type: object
result: {}
required:
- goal
title: execute_single_action参数
type: object
type: UniLabJsonCommandAsync
auto-initialize_device:
feedback: {}
goal: {}
goal_default:
device_config: null
device_id: null
handles: {}
result: {}
schema:
description: initialize_device的参数schema
properties:
feedback: {}
goal:
properties:
device_config:
type: string
device_id:
type: string
required:
- device_id
- device_config
type: object
result: {}
required:
- goal
title: initialize_device参数
type: object
type: UniLabJsonCommand
module: unilabos.ros.nodes.presets.workstation:ROS2WorkstationNode
status_types: {}
type: python
type: ros2
config_info: []
description: Workstation
handles: []
@@ -6034,10 +6636,60 @@ workstation:
init_param_schema:
config:
properties:
deck:
action_value_mappings:
type: object
children:
type: object
device_id:
type: string
driver_instance:
type: string
hardware_interface:
type: object
print_publish:
default: true
type: string
protocol_type:
items:
type: string
type: array
resource_tracker:
type: string
status_types:
type: object
required:
- deck
- protocol_type
- children
- driver_instance
- device_id
- status_types
- action_value_mappings
- hardware_interface
type: object
data:
properties: {}
required: []
type: object
version: 1.0.0
workstation.example:
category:
- work_station
class:
action_value_mappings: {}
module: unilabos.devices.workstation.workstation_base:WorkstationExample
status_types: {}
type: python
config_info: []
description: ''
handles: []
icon: ''
init_param_schema:
config:
properties:
station_resource:
type: object
required:
- station_resource
type: object
data:
properties: {}

View File

@@ -1,38 +1,36 @@
BIOYOND_PolymerStation_6VialCarrier:
category:
- bottle_carriers
class:
module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_6VialCarrier
type: pylabrobot
description: BIOYOND_PolymerStation_6VialCarrier
handles: [ ]
icon: ''
init_param_schema: { }
registry_type: resource
version: 1.0.0
BIOYOND_PolymerStation_1BottleCarrier:
category:
- bottle_carriers
- bottle_carriers
class:
module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_1BottleCarrier
type: pylabrobot
description: BIOYOND_PolymerStation_1BottleCarrier
handles: [ ]
handles: []
icon: ''
init_param_schema: { }
init_param_schema: {}
registry_type: resource
version: 1.0.0
BIOYOND_PolymerStation_1FlaskCarrier:
category:
- bottle_carriers
- bottle_carriers
class:
module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_1FlaskCarrier
type: pylabrobot
description: BIOYOND_PolymerStation_1FlaskCarrier
handles: [ ]
handles: []
icon: ''
init_param_schema: { }
init_param_schema: {}
registry_type: resource
version: 1.0.0
version: 1.0.0
BIOYOND_PolymerStation_6VialCarrier:
category:
- bottle_carriers
class:
module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_6VialCarrier
type: pylabrobot
description: BIOYOND_PolymerStation_6VialCarrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -0,0 +1,24 @@
BIOYOND_PolymerReactionStation_Deck:
category:
- deck
class:
module: unilabos.resources.bioyond.decks:BIOYOND_PolymerReactionStation_Deck
type: pylabrobot
description: BIOYOND PolymerReactionStation Deck
handles: []
icon: '反应站.webp'
init_param_schema: {}
registry_type: resource
version: 1.0.0
BIOYOND_PolymerPreparationStation_Deck:
category:
- deck
class:
module: unilabos.resources.bioyond.decks:BIOYOND_PolymerPreparationStation_Deck
type: pylabrobot
description: BIOYOND PolymerPreparationStation Deck
handles: []
icon: '配液站.webp'
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -1,6 +1,6 @@
from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder
from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder, create_ordered_items_2d
from unilabos.resources.bottle_carrier import Bottle, BottleCarrier
from unilabos.resources.itemized_carrier import Bottle, BottleCarrier
from unilabos.resources.bioyond.bottles import BIOYOND_PolymerStation_Solid_Vial, BIOYOND_PolymerStation_Solution_Beaker, BIOYOND_PolymerStation_Reagent_Bottle
# 命名约定:试剂瓶-Bottle烧杯-Beaker烧瓶-Flask小瓶-Vial
@@ -22,27 +22,29 @@ def BIOYOND_Electrolyte_6VialCarrier(name: str) -> BottleCarrier:
start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
# 创建6个位置坐标 (2行 x 3列)
locations = []
for row in range(2):
for col in range(3):
x = start_x + col * bottle_spacing_x
y = start_y + row * bottle_spacing_y
z = 5.0 # 架位底部
locations.append(Coordinate(x, y, z))
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=3,
num_items_y=2,
dx=start_x,
dy=start_y,
dz=5.0,
item_dx=bottle_spacing_x,
item_dy=bottle_spacing_y,
size_x=bottle_diameter,
size_y=bottle_diameter,
size_z=carrier_size_z,
)
for k, v in sites.items():
v.name = f"{name}_{v.name}"
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=create_homogeneous_resources(
klass=ResourceHolder,
locations=locations,
resource_size_x=bottle_diameter,
resource_size_y=bottle_diameter,
name_prefix=name,
),
sites=sites,
model="BIOYOND_Electrolyte_6VialCarrier",
)
carrier.num_items_x = 3
@@ -107,27 +109,29 @@ def BIOYOND_PolymerStation_6VialCarrier(name: str) -> BottleCarrier:
start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
# 创建6个位置坐标 (2行 x 3列)
locations = []
for row in range(2):
for col in range(3):
x = start_x + col * bottle_spacing_x
y = start_y + row * bottle_spacing_y
z = 5.0 # 架位底部
locations.append(Coordinate(x, y, z))
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=3,
num_items_y=2,
dx=start_x,
dy=start_y,
dz=5.0,
item_dx=bottle_spacing_x,
item_dy=bottle_spacing_y,
size_x=bottle_diameter,
size_y=bottle_diameter,
size_z=carrier_size_z,
)
for k, v in sites.items():
v.name = f"{name}_{v.name}"
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=create_homogeneous_resources(
klass=ResourceHolder,
locations=locations,
resource_size_x=bottle_diameter,
resource_size_y=bottle_diameter,
name_prefix=name,
),
sites=sites,
model="BIOYOND_PolymerStation_6VialCarrier",
)
carrier.num_items_x = 3

View File

@@ -1,4 +1,4 @@
from unilabos.resources.bottle_carrier import Bottle, BottleCarrier
from unilabos.resources.itemized_carrier import Bottle, BottleCarrier
# 工厂函数

View File

@@ -1,12 +1,21 @@
from pylabrobot.resources import Deck, Coordinate
from pylabrobot.resources import Deck, Coordinate, Rotation
from unilabos.resources.bioyond.warehouses import bioyond_warehouse_1x4x4, bioyond_warehouse_1x4x2, bioyond_warehouse_liquid_and_lid_handling
class BIOYOND_PolymerReactionStation_Deck(Deck):
def __init__(self, name: str = "PolymerReactionStation_Deck") -> None:
def __init__(
self,
name: str = "PolymerReactionStation_Deck",
size_x: float = 2700.0,
size_y: float = 1080.0,
size_z: float = 1500.0,
category: str = "deck",
setup: bool = False
) -> None:
super().__init__(name=name, size_x=2700.0, size_y=1080.0, size_z=1500.0)
self.setup()
if setup:
self.setup()
def setup(self) -> None:
# 添加仓库
@@ -16,20 +25,29 @@ class BIOYOND_PolymerReactionStation_Deck(Deck):
"站内试剂存放堆栈": bioyond_warehouse_liquid_and_lid_handling("站内试剂存放堆栈"),
}
self.warehouse_locations = {
"堆栈1": Coordinate(0.0, 650.0, 0.0),
"堆栈2": Coordinate(2550.0, 650.0, 0.0),
"堆栈1": Coordinate(0.0, 430.0, 0.0),
"堆栈2": Coordinate(2550.0, 430.0, 0.0),
"站内试剂存放堆栈": Coordinate(800.0, 475.0, 0.0),
}
self.warehouses["站内试剂存放堆栈"].rotation = 90.0
self.warehouses["站内试剂存放堆栈"].rotation = Rotation(z=90)
for warehouse_name, warehouse in self.warehouses.items():
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
class BIOYOND_PolymerPreparationStation_Deck(Deck):
def __init__(self, name: str = "PolymerPreparationStation_Deck") -> None:
def __init__(
self,
name: str = "PolymerPreparationStation_Deck",
size_x: float = 2700.0,
size_y: float = 1080.0,
size_z: float = 1500.0,
category: str = "deck",
setup: bool = False
) -> None:
super().__init__(name=name, size_x=2700.0, size_y=1080.0, size_z=1500.0)
self.warehouses = {}
if setup:
self.setup()
def setup(self) -> None:
# 添加仓库

View File

@@ -1,9 +1,9 @@
from unilabos.resources.warehouse import WareHouse
from unilabos.resources.warehouse import WareHouse, warehouse_factory
def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
"""创建BioYond 4x1x4仓库"""
return WareHouse(
return warehouse_factory(
name=name,
num_items_x=1,
num_items_y=4,
@@ -20,7 +20,7 @@ def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
def bioyond_warehouse_1x4x2(name: str) -> WareHouse:
"""创建BioYond 4x1x2仓库"""
return WareHouse(
return warehouse_factory(
name=name,
num_items_x=1,
num_items_y=4,
@@ -38,7 +38,7 @@ def bioyond_warehouse_1x4x2(name: str) -> WareHouse:
def bioyond_warehouse_liquid_and_lid_handling(name: str) -> WareHouse:
"""创建BioYond开关盖加液模块台面"""
return WareHouse(
return warehouse_factory(
name=name,
num_items_x=2,
num_items_y=5,

View File

@@ -1,72 +0,0 @@
"""
自动化液体处理工作站物料类定义 - 简化版
Automated Liquid Handling Station Resource Classes - Simplified Version
"""
from __future__ import annotations
from typing import Dict, Optional
from pylabrobot.resources.coordinate import Coordinate
from pylabrobot.resources.container import Container
from pylabrobot.resources.carrier import TubeCarrier
from pylabrobot.resources.resource_holder import ResourceHolder
class Bottle(Container):
"""瓶子类 - 简化版,不追踪瓶盖"""
def __init__(
self,
name: str,
diameter: float,
height: float,
max_volume: float,
barcode: Optional[str] = "",
category: str = "container",
model: Optional[str] = None,
):
super().__init__(
name=name,
size_x=diameter,
size_y=diameter,
size_z=height,
max_volume=max_volume,
category=category,
model=model,
)
self.diameter = diameter
self.height = height
self.barcode = barcode
def serialize(self) -> dict:
return {
**super().serialize(),
"diameter": self.diameter,
"height": self.height,
"barcode": self.barcode,
}
class BottleCarrier(TubeCarrier):
"""瓶载架 - 直接继承自 TubeCarrier"""
def __init__(
self,
name: str,
size_x: float,
size_y: float,
size_z: float,
sites: Optional[Dict[int, ResourceHolder]] = None,
category: str = "bottle_carrier",
model: Optional[str] = None,
):
super().__init__(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
sites=sites,
category=category,
model=model,
)

View File

@@ -4,6 +4,7 @@ import json
from typing import Union, Any, Dict
import numpy as np
import networkx as nx
from pylabrobot.resources import ResourceHolder
from unilabos_msgs.msg import Resource
from unilabos.resources.container import RegularContainer
@@ -507,7 +508,7 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: dict =
number = (detail.get("z", 0) - 1) * plr_material.num_items_x * plr_material.num_items_y + \
(detail.get("x", 0) - 1) * plr_material.num_items_x + \
(detail.get("y", 0) - 1)
bottle = plr_material[number].resource
bottle = plr_material[number]
bottle.code = detail.get("code", "")
bottle.tracker.liquids = [(detail["name"], float(detail.get("quantity", 0)) if detail.get("quantity") else 0)]
@@ -520,8 +521,8 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: dict =
idx = (loc.get("y", 0) - 1) * warehouse.num_items_x * warehouse.num_items_y + \
(loc.get("x", 0) - 1) * warehouse.num_items_x + \
(loc.get("z", 0) - 1)
if 0 <= idx < warehouse.num_items_x * warehouse.num_items_y * warehouse.num_items_z:
if warehouse[idx].resource is None:
if 0 <= idx < warehouse.capacity:
if warehouse[idx] is None or isinstance(warehouse[idx], ResourceHolder):
warehouse[idx] = plr_material
return plr_materials

View File

@@ -0,0 +1,357 @@
"""
自动化液体处理工作站物料类定义 - 简化版
Automated Liquid Handling Station Resource Classes - Simplified Version
"""
from __future__ import annotations
from typing import Dict, Optional
from pylabrobot.resources.coordinate import Coordinate
from pylabrobot.resources.container import Container
from pylabrobot.resources.resource_holder import ResourceHolder
from pylabrobot.resources import Resource as ResourcePLR
class Bottle(Container):
"""瓶子类 - 简化版,不追踪瓶盖"""
def __init__(
self,
name: str,
diameter: float,
height: float,
max_volume: float,
size_x: float = 0.0,
size_y: float = 0.0,
size_z: float = 0.0,
barcode: Optional[str] = "",
category: str = "container",
model: Optional[str] = None,
):
super().__init__(
name=name,
size_x=diameter,
size_y=diameter,
size_z=height,
max_volume=max_volume,
category=category,
model=model,
)
self.diameter = diameter
self.height = height
self.barcode = barcode
def serialize(self) -> dict:
return {
**super().serialize(),
"diameter": self.diameter,
"height": self.height,
"barcode": self.barcode,
}
from string import ascii_uppercase as LETTERS
from typing import Dict, List, Optional, Type, TypeVar, Union, Sequence, Tuple
import pylabrobot
from pylabrobot.resources.resource_holder import ResourceHolder
T = TypeVar("T", bound=ResourceHolder)
S = TypeVar("S", bound=ResourceHolder)
class ItemizedCarrier(ResourcePLR):
"""Base class for all carriers."""
def __init__(
self,
name: str,
size_x: float,
size_y: float,
size_z: float,
num_items_x: int = 0,
num_items_y: int = 0,
num_items_z: int = 0,
sites: Optional[Dict[Union[int, str], Optional[ResourcePLR]]] = None,
category: Optional[str] = "carrier",
model: Optional[str] = None,
):
super().__init__(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
category=category,
model=model,
)
self.num_items = len(sites)
self.num_items_x, self.num_items_y, self.num_items_z = num_items_x, num_items_y, num_items_z
if isinstance(sites, dict):
sites = sites or {}
self.sites: List[Optional[ResourcePLR]] = list(sites.values())
self._ordering = sites
self.child_locations: Dict[str, Coordinate] = {}
self.child_size: Dict[str, dict] = {}
for spot, resource in sites.items():
if resource is not None and getattr(resource, "location", None) is None:
raise ValueError(f"resource {resource} has no location")
if resource is not None:
self.child_locations[spot] = resource.location
self.child_size[spot] = {"width": resource._size_x, "height": resource._size_y, "depth": resource._size_z}
else:
self.child_locations[spot] = Coordinate.zero()
self.child_size[spot] = {"width": 0, "height": 0, "depth": 0}
elif isinstance(sites, list):
# deserialize时走这里还需要根据 self.sites 索引children
self.child_locations = {site["label"]: Coordinate(**site["position"]) for site in sites}
self.child_size = {site["label"]: site["size"] for site in sites}
self.sites = [site["occupied_by"] for site in sites]
self._ordering = {site["label"]: site["position"] for site in sites}
else:
print("sites:", sites)
@property
def capacity(self):
"""The number of sites on this carrier."""
return len(self.sites)
def __len__(self) -> int:
"""Return the number of sites on this carrier."""
return len(self.sites)
def assign_child_resource(
self,
resource: ResourcePLR,
location: Optional[Coordinate],
reassign: bool = True,
spot: Optional[int] = None,
):
idx = spot
# 如果只给 location根据坐标和 deserialize 后的 self.sites持有names来寻找 resource 该摆放的位置
if spot is not None:
idx = spot
else:
for i, site in enumerate(self.sites):
site_location = list(self.child_locations.values())[i]
if type(site) == str and site == resource.name:
idx = i
break
if site_location == location:
idx = i
break
if not reassign and self.sites[idx] is not None:
raise ValueError(f"a site with index {idx} already exists")
super().assign_child_resource(resource, location=location, reassign=reassign)
self.sites[idx] = resource
def assign_resource_to_site(self, resource: ResourcePLR, spot: int):
if self.sites[spot] is not None and not isinstance(self.sites[spot], ResourceHolder):
raise ValueError(f"spot {spot} already has a resource, {resource}")
self.assign_child_resource(resource, location=self.child_locations.get(str(spot)), spot=spot)
def unassign_child_resource(self, resource: ResourcePLR):
found = False
for spot, res in enumerate(self.sites):
if res == resource:
self.sites[spot] = None
found = True
break
if not found:
raise ValueError(f"Resource {resource} is not assigned to this carrier")
if hasattr(resource, "unassign"):
resource.unassign()
def __getitem__(
self,
identifier: Union[str, int, Sequence[int], Sequence[str], slice, range],
) -> Union[List[T], T]:
"""Get the items with the given identifier.
This is a convenience method for getting the items with the given identifier. It is equivalent
to :meth:`get_items`, but adds support for slicing and supports single items in the same
functional call. Note that the return type will always be a list, even if a single item is
requested.
Examples:
Getting the items with identifiers "A1" through "E1":
>>> items["A1:E1"]
[<Item A1>, <Item B1>, <Item C1>, <Item D1>, <Item E1>]
Getting the items with identifiers 0 through 4 (note that this is the same as above):
>>> items[range(5)]
[<Item A1>, <Item B1>, <Item C1>, <Item D1>, <Item E1>]
Getting items with a slice (note that this is the same as above):
>>> items[0:5]
[<Item A1>, <Item B1>, <Item C1>, <Item D1>, <Item E1>]
Getting a single item:
>>> items[0]
[<Item A1>]
"""
if isinstance(identifier, str):
if ":" in identifier: # multiple # TODO: deprecate this, use `"A1":"E1"` instead (slice)
return self.get_items(identifier)
return self.get_item(identifier) # single
if isinstance(identifier, int):
return self.get_item(identifier)
if isinstance(identifier, (slice, range)):
start, stop = identifier.start, identifier.stop
if isinstance(identifier.start, str):
start = list(self._ordering.keys()).index(identifier.start)
elif identifier.start is None:
start = 0
if isinstance(identifier.stop, str):
stop = list(self._ordering.keys()).index(identifier.stop)
elif identifier.stop is None:
stop = self.num_items
identifier = list(range(start, stop, identifier.step or 1))
return self.get_items(identifier)
if isinstance(identifier, (list, tuple)):
return self.get_items(identifier)
raise TypeError(f"Invalid identifier type: {type(identifier)}")
def get_item(self, identifier: Union[str, int, Tuple[int, int]]) -> T:
"""Get the item with the given identifier.
Args:
identifier: The identifier of the item. Either a string, an integer, or a tuple. If an
integer, it is the index of the item in the list of items (counted from 0, top to bottom, left
to right). If a string, it uses transposed MS Excel style notation, e.g. "A1" for the first
item, "B1" for the item below that, etc. If a tuple, it is (row, column).
Raises:
IndexError: If the identifier is out of range. The range is 0 to self.num_items-1 (inclusive).
"""
if isinstance(identifier, tuple):
row, column = identifier
identifier = LETTERS[row] + str(column + 1) # standard transposed-Excel style notation
if isinstance(identifier, str):
try:
identifier = list(self._ordering.keys()).index(identifier)
except ValueError as e:
raise IndexError(
f"Item with identifier '{identifier}' does not exist on " f"resource '{self.name}'."
) from e
if not 0 <= identifier < self.capacity:
raise IndexError(
f"Item with identifier '{identifier}' does not exist on " f"resource '{self.name}'."
)
# Cast child to item type. Children will always be `T`, but the type checker doesn't know that.
return self.sites[identifier]
def get_items(self, identifiers: Union[str, Sequence[int], Sequence[str]]) -> List[T]:
"""Get the items with the given identifier.
Args:
identifier: Deprecated. Use `identifiers` instead. # TODO(deprecate-ordered-items)
identifiers: The identifiers of the items. Either a string range or a list of integers. If a
string, it uses transposed MS Excel style notation. Regions of items can be specified using
a colon, e.g. "A1:H1" for the first column. If a list of integers, it is the indices of the
items in the list of items (counted from 0, top to bottom, left to right).
Examples:
Getting the items with identifiers "A1" through "E1":
>>> items.get_items("A1:E1")
[<Item A1>, <Item B1>, <Item C1>, <Item D1>, <Item E1>]
Getting the items with identifiers 0 through 4:
>>> items.get_items(range(5))
[<Item A1>, <Item B1>, <Item C1>, <Item D1>, <Item E1>]
"""
if isinstance(identifiers, str):
identifiers = pylabrobot.utils.expand_string_range(identifiers)
return [self.get_item(i) for i in identifiers]
def __setitem__(self, idx: Union[int, str], resource: Optional[ResourcePLR]):
"""Assign a resource to this carrier."""
if resource is None: # setting to None
assigned_resource = self[idx]
if assigned_resource is not None:
self.unassign_child_resource(assigned_resource)
else:
idx = list(self._ordering.keys()).index(idx) if isinstance(idx, str) else idx
self.assign_resource_to_site(resource, spot=idx)
def __delitem__(self, idx: int):
"""Unassign a resource from this carrier."""
assigned_resource = self[idx]
if assigned_resource is not None:
self.unassign_child_resource(assigned_resource)
def get_resources(self) -> List[ResourcePLR]:
"""Get all resources assigned to this carrier."""
return [resource for resource in self.sites.values() if resource is not None]
def __eq__(self, other):
return super().__eq__(other) and self.sites == other.sites
def get_free_sites(self) -> List[int]:
return [spot for spot, resource in self.sites.items() if resource is None]
def serialize(self):
return {
**super().serialize(),
"num_items_x": self.num_items_x,
"num_items_y": self.num_items_y,
"num_items_z": self.num_items_z,
"sites": [{
"label": str(identifier),
"visible": True if self[identifier] is not None else False,
"occupied_by": self[identifier].name
if isinstance(self[identifier], ResourcePLR) and not isinstance(self[identifier], ResourceHolder) else
self[identifier] if isinstance(self[identifier], str) else None,
"position": {"x": location.x, "y": location.y, "z": location.z},
"size": self.child_size[identifier],
"content_type": ["bottle", "container", "tube", "bottle_carrier", "tip_rack"]
} for identifier, location in self.child_locations.items()]
}
class BottleCarrier(ItemizedCarrier):
"""瓶载架 - 直接继承自 TubeCarrier"""
def __init__(
self,
name: str,
size_x: float,
size_y: float,
size_z: float,
sites: Optional[Dict[Union[int, str], ResourceHolder]] = None,
category: str = "bottle_carrier",
model: Optional[str] = None,
):
super().__init__(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
sites=sites,
category=category,
model=model,
)

View File

@@ -1,72 +1,94 @@
import json
from typing import Optional, List
from pylabrobot.resources import Coordinate, Resource
from pylabrobot.resources.carrier import Carrier, PlateHolder, ResourceHolder, create_homogeneous_resources
from pylabrobot.resources.deck import Deck
from typing import Dict, Optional, List, Union
from pylabrobot.resources import Coordinate
from pylabrobot.resources.carrier import ResourceHolder, create_homogeneous_resources
from unilabos.resources.itemized_carrier import ItemizedCarrier, ResourcePLR
class WareHouse(Carrier[ResourceHolder]):
"""4x4x1堆栈载体类 - 可容纳16个板位的载体4层x4行x1列"""
def warehouse_factory(
name: str,
num_items_x: int = 4,
num_items_y: int = 1,
num_items_z: int = 4,
dx: float = 137.0,
dy: float = 96.0,
dz: float = 120.0,
item_dx: float = 10.0,
item_dy: float = 10.0,
item_dz: float = 10.0,
removed_positions: Optional[List[int]] = None,
empty: bool = False,
category: str = "warehouse",
model: Optional[str] = None,
):
# 创建16个板架位 (4层 x 4位置)
locations = []
for layer in range(num_items_z): # 4层
for row in range(num_items_y): # 4行
for col in range(num_items_x): # 1列 (每层4x1=4个位置)
# 计算位置
x = dx + col * item_dx
y = dy + (num_items_y - row - 1) * item_dy
z = dz + (num_items_z - layer - 1) * item_dz
locations.append(Coordinate(x, y, z))
if removed_positions:
locations = [loc for i, loc in enumerate(locations) if i not in removed_positions]
sites = create_homogeneous_resources(
klass=ResourceHolder,
locations=locations,
resource_size_x=127.0,
resource_size_y=86.0,
name_prefix=name,
)
return WareHouse(
name=name,
size_x=dx + item_dx * num_items_x,
size_y=dy + item_dy * num_items_y,
size_z=dz + item_dz * num_items_z,
num_items_x = num_items_x,
num_items_y = num_items_y,
num_items_z = num_items_z,
# ordered_items=ordered_items,
# ordering=ordering,
sites=sites,
category=category,
model=model,
)
class WareHouse(ItemizedCarrier):
"""堆栈载体类 - 可容纳16个板位的载体4层x4行x1列"""
def __init__(
self,
name: str,
num_items_x: int = 4,
num_items_y: int = 1,
num_items_z: int = 4,
dx: float = 137.0,
dy: float = 96.0,
dz: float = 120.0,
item_dx: float = 10.0,
item_dy: float = 10.0,
item_dz: float = 10.0,
removed_positions: Optional[List[int]] = None,
size_x: float,
size_y: float,
size_z: float,
num_items_x: int,
num_items_y: int,
num_items_z: int,
sites: Optional[Dict[Union[int, str], Optional[ResourcePLR]]] = None,
category: str = "warehouse",
model: Optional[str] = None,
):
self.num_items_x = num_items_x
self.num_items_y = num_items_y
self.num_items_z = num_items_z
# 创建16个板架位 (4层 x 4位置)
locations = []
for layer in range(num_items_z): # 4层
for row in range(num_items_y): # 4行
for col in range(num_items_x): # 1列 (每层4x1=4个位置)
# 计算位置
x = dx + col * item_dx
y = dy + (num_items_y - row - 1) * item_dy
z = dz + (num_items_z - layer - 1) * item_dz
locations.append(Coordinate(x, y, z))
if removed_positions:
locations = [loc for i, loc in enumerate(locations) if i not in removed_positions]
sites = create_homogeneous_resources(
klass=ResourceHolder,
locations=[
Coordinate(4.0, 8.5, 86.15),
Coordinate(4.0, 104.5, 86.15),
Coordinate(4.0, 200.5, 86.15),
Coordinate(4.0, 296.5, 86.15),
Coordinate(4.0, 392.5, 86.15),
],
resource_size_x=127.0,
resource_size_y=86.0,
name_prefix=name,
)
super().__init__(
name=name,
size_x=dx + item_dx * num_items_x,
size_y=dy + item_dy * num_items_y,
size_z=dz + item_dz * num_items_z,
size_x=size_x,
size_y=size_y,
size_z=size_z,
# ordered_items=ordered_items,
# ordering=ordering,
num_items_x=num_items_x,
num_items_y=num_items_y,
num_items_z=num_items_z,
sites=sites,
category=category,
model=model,
)
def get_site_by_layer_position(self, row: int, col: int, layer: int) -> PlateHolder:
def get_site_by_layer_position(self, row: int, col: int, layer: int) -> ResourceHolder:
if not (0 <= layer < 4 and 0 <= row < 4 and 0 <= col < 1):
raise ValueError("无效的位置: layer={}, row={}, col={}".format(layer, row, col))

View File

@@ -103,10 +103,14 @@ set(action_files
"action/PostProcessGrab.action"
"action/PostProcessTriggerClean.action"
"action/PostProcessTriggerPostPro.action"
"action/ReactionStationDripBack.action"
"action/ReactionStationLiquidFeed.action"
"action/ReactionStationLiquidFeedBeaker.action"
"action/ReactionStationLiquidFeedSolvents.action"
"action/ReactionStationLiquidFeedTitration.action"
"action/ReactionStationLiquidFeedVialsNonTitration.action"
"action/ReactionStationProExecu.action"
"action/ReactionStationReactorTakenOut.action"
"action/ReactionStationReaTackIn.action"
"action/ReactionStationSolidFeedVial.action"
)

View File

@@ -1,11 +1,13 @@
# Goal - 滴回去
string volume # 投料体积
string assign_material_name # 溶剂名称
string time # 观察时间单位min
string torque_variation #是否观察1否2是
# Goal - 滴回去操作参数
string volume # 投料体积
string assign_material_name # 溶剂名称
string time # 观察时间单位min
string torque_variation # 是否观察1否2是
---
# Result - 操作结果
string return_info # 结果消息
# Result - 操作结果
bool success # 操作是否成功
string return_info # 结果消息
int32 code # 操作结果代码1表示成功0表示失败
---
# Feedback - 实时反馈
# Feedback - 实时反馈
string feedback # 操作过程中的反馈信息

View File

@@ -1,11 +0,0 @@
# Goal - 液体投料
string titration_type # 滴定类型1否2是
string volume # 投料体积
string assign_material_name # 溶剂名称
string time # 观察时间单位min
string torque_variation #是否观察1否2是
---
# Result - 操作结果
string return_info # 结果消息
---
# Feedback - 实时反馈

View File

@@ -0,0 +1,15 @@
# Goal - 液体投料-烧杯操作参数
string volume # 投料体积
string assign_material_name # 溶剂名称
string titration_type # 滴定类型1否2是
string time # 观察时间单位min
string torque_variation # 是否观察1否2是
string temperature # 温度设置
---
# Result - 操作结果
bool success # 操作是否成功
string return_info # 结果消息
int32 code # 操作结果代码1表示成功0表示失败
---
# Feedback - 实时反馈
string feedback # 操作过程中的反馈信息

View File

@@ -0,0 +1,15 @@
# Goal - 液体投料-溶剂操作参数
string volume # 投料体积
string assign_material_name # 溶剂名称
string titration_type # 滴定类型1否2是
string time # 观察时间单位min
string torque_variation # 是否观察1否2是
string temperature # 温度设置
---
# Result - 操作结果
bool success # 操作是否成功
string return_info # 结果消息
int32 code # 操作结果代码1表示成功0表示失败
---
# Feedback - 实时反馈
string feedback # 操作过程中的反馈信息

View File

@@ -0,0 +1,15 @@
# Goal - 液体投料滴定操作参数
string volume_formula # 投料体积公式
string assign_material_name # 溶剂名称
string titration_type # 滴定类型1否2是
string time # 观察时间单位min
string torque_variation # 是否观察1否2是
string temperature # 温度设置
---
# Result - 操作结果
bool success # 操作是否成功
string return_info # 结果消息
int32 code # 操作结果代码1表示成功0表示失败
---
# Feedback - 实时反馈
string feedback # 操作过程中的反馈信息

View File

@@ -0,0 +1,15 @@
# Goal - 液体投料-小瓶非滴定操作参数
string volume_formula # 投料体积公式
string assign_material_name # 溶剂名称
string titration_type # 滴定类型1否2是
string time # 观察时间单位min
string torque_variation # 是否观察1否2是
string temperature # 温度设置
---
# Result - 操作结果
bool success # 操作是否成功
string return_info # 结果消息
int32 code # 操作结果代码1表示成功0表示失败
---
# Feedback - 实时反馈
string feedback # 操作过程中的反馈信息

View File

@@ -1,8 +1,11 @@
# Goal - 合并工作流+执行
string workflow_name # 工作流名称
string task_name # 任务名称
# Goal - 合并工作流+执行参数
string workflow_name # 工作流名称
string task_name # 任务名称
---
# Result - 操作结果
string return_info # 结果消息
# Result - 操作结果
bool success # 操作是否成功
string return_info # 结果消息
int32 code # 操作结果代码1表示成功0表示失败
---
# Feedback - 实时反馈
# Feedback - 实时反馈
string feedback # 操作过程中的反馈信息

View File

@@ -1,9 +1,12 @@
# Goal - 通量-配置
string cutoff # 黏度_通量-配置
string temperature # 温度_通量-配
string assign_material_name # 分液类型_通量-配置
# Goal - 反应器放入操作参数
string cutoff # 黏度设置
string temperature # 温度
string assign_material_name # 分液类型
---
# Result - 操作结果
string return_info # 结果消息
# Result - 操作结果
bool success # 操作是否成功
string return_info # 结果消息
int32 code # 操作结果代码1表示成功0表示失败
---
# Feedback - 实时反馈
# Feedback - 实时反馈
string feedback # 操作过程中的反馈信息

View File

@@ -0,0 +1,12 @@
# Goal - 反应器取出操作参数
# 反应器取出操作不需要任何参数
---
# Result - 操作结果
# 反应器取出操作的结果
bool success # 要求必须包含success以便回传执行结果
string return_info # 要求必须包含return_info以便回传执行结果
int32 code # 操作结果代码1表示成功0表示失败
---
# Feedback - 实时反馈
# 反应器取出操作的反馈
string feedback # 操作过程中的反馈信息

View File

@@ -1,10 +1,13 @@
# Goal - 固体投料-小瓶
string assign_material_name # 固体名称_粉末加样模块-投料
string material_id # 固体投料类型_粉末加样模块-投料
string time # 观察时间_反应模块-观察搅拌结果
string torque_variation #是否观察1否2是_反应模块-观察搅拌结果
# Goal - 固体投料-小瓶操作参数
string assign_material_name # 固体名称
string material_id # 固体投料类型
string time # 观察时间单位min
string torque_variation # 是否观察1否2是
---
# Result - 操作结果
string return_info # 结果消息
# Result - 操作结果
bool success # 操作是否成功
string return_info # 结果消息
int32 code # 操作结果代码1表示成功0表示失败
---
# Feedback - 实时反馈
# Feedback - 实时反馈
string feedback # 操作过程中的反馈信息