mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-15 13:44:39 +00:00
Compare commits
4 Commits
fa9b2a08f2
...
workstatio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5805f94e9a | ||
|
|
3adcc41ce8 | ||
|
|
243922caf4 | ||
|
|
079ec9d1b4 |
BIN
unilabos/devices/workstation/bioyond_cell/2025092702.xlsx
Normal file
BIN
unilabos/devices/workstation/bioyond_cell/2025092702.xlsx
Normal file
Binary file not shown.
BIN
unilabos/devices/workstation/bioyond_cell/2025101301.xlsx
Normal file
BIN
unilabos/devices/workstation/bioyond_cell/2025101301.xlsx
Normal file
Binary file not shown.
49
unilabos/devices/workstation/bioyond_cell/benyao_test.py
Normal file
49
unilabos/devices/workstation/bioyond_cell/benyao_test.py
Normal 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()
|
||||
@@ -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
|
||||
796
unilabos/devices/workstation/bioyond_cell/bioyond_workstation.py
Normal file
796
unilabos/devices/workstation/bioyond_cell/bioyond_workstation.py
Normal 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"])
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
706
unilabos/devices/workstation/bioyond_cell/cellconfig3c.json
Normal file
706
unilabos/devices/workstation/bioyond_cell/cellconfig3c.json
Normal 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": []
|
||||
}
|
||||
20
unilabos/devices/workstation/bioyond_cell/cellconfig3ca.json
Normal file
20
unilabos/devices/workstation/bioyond_cell/cellconfig3ca.json
Normal 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": []
|
||||
}
|
||||
BIN
unilabos/devices/workstation/bioyond_cell/样品导入模板.xlsx
Normal file
BIN
unilabos/devices/workstation/bioyond_cell/样品导入模板.xlsx
Normal file
Binary file not shown.
374
unilabos/devices/workstation/bioyond_material_management.py
Normal file
374
unilabos/devices/workstation/bioyond_material_management.py
Normal 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
|
||||
@@ -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进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等)
|
||||
data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等)
|
||||
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
1332
unilabos/devices/workstation/coin_cell_assembly/celljson.json
Normal file
1332
unilabos/devices/workstation/coin_cell_assembly/celljson.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
@@ -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
|
||||
|
@@ -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,
|
||||
|
1925
unilabos/devices/workstation/coin_cell_assembly/new_cellconfig.json
Normal file
1925
unilabos/devices/workstation/coin_cell_assembly/new_cellconfig.json
Normal file
File diff suppressed because it is too large
Load Diff
1925
unilabos/devices/workstation/coin_cell_assembly/new_cellconfig2.json
Normal file
1925
unilabos/devices/workstation/coin_cell_assembly/new_cellconfig2.json
Normal file
File diff suppressed because it is too large
Load Diff
1925
unilabos/devices/workstation/coin_cell_assembly/new_cellconfig3.json
Normal file
1925
unilabos/devices/workstation/coin_cell_assembly/new_cellconfig3.json
Normal file
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
@@ -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": []
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
6674
unilabos/devices/workstation/coin_cell_assembly/work_station.yaml
Normal file
6674
unilabos/devices/workstation/coin_cell_assembly/work_station.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -149,22 +149,6 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
||||
)
|
||||
self._send_response(error_response)
|
||||
|
||||
def do_OPTIONS(self):
|
||||
"""处理OPTIONS请求 - CORS预检请求"""
|
||||
try:
|
||||
# 发送CORS响应头
|
||||
self.send_response(200)
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
||||
self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
|
||||
self.send_header('Access-Control-Max-Age', '86400')
|
||||
self.end_headers()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"OPTIONS请求处理失败: {e}")
|
||||
self.send_response(500)
|
||||
self.end_headers()
|
||||
|
||||
def _handle_step_finish_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
||||
"""处理步骤完成报送(统一LIMS协议规范)"""
|
||||
try:
|
||||
@@ -222,7 +206,7 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
||||
|
||||
# 验证data字段内容
|
||||
data = request_data['data']
|
||||
data_required_fields = ['orderCode', 'orderName', 'sampleId', 'startTime', 'endTime', 'status']
|
||||
data_required_fields = ['orderCode', 'orderName', 'sampleId', 'startTime', 'endTime', 'Status']
|
||||
if data_missing_fields := [field for field in data_required_fields if field not in data]:
|
||||
return HttpResponse(
|
||||
success=False,
|
||||
@@ -243,7 +227,7 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
||||
"0": "待生产", "2": "进样", "10": "开始",
|
||||
"20": "完成", "-2": "异常停止", "-3": "人工停止"
|
||||
}
|
||||
status_desc = status_names.get(str(data['status']), f"状态{data['status']}")
|
||||
status_desc = status_names.get(str(data['Status']), f"状态{data['Status']}")
|
||||
|
||||
return HttpResponse(
|
||||
success=True,
|
||||
@@ -396,21 +380,6 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
||||
"""处理物料变更报送"""
|
||||
try:
|
||||
# 验证必需字段
|
||||
if 'brand' in request_data:
|
||||
if request_data['brand'] == "bioyond": # 奔曜
|
||||
error_msg = request_data["text"]
|
||||
logger.info(f"收到奔曜错误处理报送: {error_msg}")
|
||||
return HttpResponse(
|
||||
success=True,
|
||||
message=f"错误处理报送已收到: {error_msg}",
|
||||
acknowledgment_id=f"ERROR_{int(time.time() * 1000)}_{error_msg.get('action_id', 'unknown')}",
|
||||
data=None
|
||||
)
|
||||
else:
|
||||
return HttpResponse(
|
||||
success=False,
|
||||
message=f"缺少厂家信息(brand字段)"
|
||||
)
|
||||
required_fields = ['workstation_id', 'timestamp', 'resource_id', 'change_type']
|
||||
if missing_fields := [field for field in required_fields if field not in request_data]:
|
||||
return HttpResponse(
|
||||
@@ -438,45 +407,23 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
||||
def _handle_error_handling_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
||||
"""处理错误处理报送"""
|
||||
try:
|
||||
# 检查是否为奔曜格式的错误报送
|
||||
if 'brand' in request_data and str(request_data['brand']).lower() == "bioyond":
|
||||
# 奔曜格式处理
|
||||
if 'text' not in request_data:
|
||||
return HttpResponse(
|
||||
success=False,
|
||||
message="奔曜格式缺少text字段"
|
||||
)
|
||||
|
||||
error_data = request_data["text"]
|
||||
logger.info(f"收到奔曜错误处理报送: {error_data}")
|
||||
|
||||
# 调用工作站的处理方法
|
||||
result = self.workstation.handle_external_error(error_data)
|
||||
|
||||
# 验证必需字段
|
||||
required_fields = ['workstation_id', 'timestamp', 'error_type', 'error_message']
|
||||
if missing_fields := [field for field in required_fields if field not in request_data]:
|
||||
return HttpResponse(
|
||||
success=True,
|
||||
message=f"错误处理报送已收到: 任务{error_data.get('task', 'unknown')}, 错误代码{error_data.get('code', 'unknown')}",
|
||||
acknowledgment_id=f"ERROR_{int(time.time() * 1000)}_{error_data.get('task', 'unknown')}",
|
||||
data=result
|
||||
)
|
||||
else:
|
||||
# 标准格式处理
|
||||
required_fields = ['workstation_id', 'timestamp', 'error_type', 'error_message']
|
||||
if missing_fields := [field for field in required_fields if field not in request_data]:
|
||||
return HttpResponse(
|
||||
success=False,
|
||||
message=f"缺少必要字段: {', '.join(missing_fields)}"
|
||||
)
|
||||
|
||||
# 调用工作站的处理方法
|
||||
result = self.workstation.handle_external_error(request_data)
|
||||
|
||||
return HttpResponse(
|
||||
success=True,
|
||||
message=f"错误处理报送已处理: {request_data['error_type']} - {request_data['error_message']}",
|
||||
acknowledgment_id=f"ERROR_{int(time.time() * 1000)}_{request_data.get('action_id', 'unknown')}",
|
||||
data=result
|
||||
success=False,
|
||||
message=f"缺少必要字段: {', '.join(missing_fields)}"
|
||||
)
|
||||
|
||||
# 调用工作站的处理方法
|
||||
result = self.workstation.handle_external_error(request_data)
|
||||
|
||||
return HttpResponse(
|
||||
success=True,
|
||||
message=f"错误处理报送已处理: {request_data['error_type']} - {request_data['error_message']}",
|
||||
acknowledgment_id=f"ERROR_{int(time.time() * 1000)}_{request_data.get('action_id', 'unknown')}",
|
||||
data=result
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理错误处理报送失败: {e}")
|
||||
@@ -601,19 +548,13 @@ class WorkstationHTTPService:
|
||||
"""停止HTTP服务"""
|
||||
try:
|
||||
if self.running and self.server:
|
||||
logger.info("正在停止工作站HTTP报送服务...")
|
||||
self.running = False
|
||||
|
||||
# 停止serve_forever循环
|
||||
self.server.shutdown()
|
||||
self.server.server_close()
|
||||
|
||||
# 等待服务器线程结束
|
||||
if self.server_thread and self.server_thread.is_alive():
|
||||
self.server_thread.join(timeout=5.0)
|
||||
|
||||
# 关闭服务器套接字
|
||||
self.server.server_close()
|
||||
|
||||
logger.info("工作站HTTP报送服务已停止")
|
||||
|
||||
except Exception as e:
|
||||
@@ -622,13 +563,11 @@ class WorkstationHTTPService:
|
||||
def _run_server(self):
|
||||
"""运行HTTP服务器"""
|
||||
try:
|
||||
# 使用serve_forever()让服务持续运行
|
||||
self.server.serve_forever()
|
||||
while self.running:
|
||||
self.server.handle_request()
|
||||
except Exception as e:
|
||||
if self.running: # 只在非正常停止时记录错误
|
||||
logger.error(f"HTTP服务运行错误: {e}")
|
||||
finally:
|
||||
logger.info("HTTP服务器线程已退出")
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
@@ -664,49 +603,3 @@ __all__ = [
|
||||
'MaterialChangeReport',
|
||||
'TaskExecutionReport'
|
||||
]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 简单测试HTTP服务
|
||||
class DummyWorkstation:
|
||||
device_id = "WS-001"
|
||||
|
||||
def process_step_finish_report(self, report_request):
|
||||
return {"processed": True}
|
||||
|
||||
def process_sample_finish_report(self, report_request):
|
||||
return {"processed": True}
|
||||
|
||||
def process_order_finish_report(self, report_request, used_materials):
|
||||
return {"processed": True}
|
||||
|
||||
def process_material_change_report(self, report_data):
|
||||
return {"processed": True}
|
||||
|
||||
def handle_external_error(self, error_data):
|
||||
return {"handled": True}
|
||||
|
||||
workstation = DummyWorkstation()
|
||||
http_service = WorkstationHTTPService(workstation)
|
||||
|
||||
try:
|
||||
http_service.start()
|
||||
print(f"测试服务器已启动: {http_service.service_url}")
|
||||
print("按 Ctrl+C 停止服务器")
|
||||
print("服务将持续运行,等待接收HTTP请求...")
|
||||
|
||||
# 保持服务器运行 - 使用更好的等待机制
|
||||
try:
|
||||
while http_service.is_running:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
print("\n接收到停止信号...")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n正在停止服务器...")
|
||||
http_service.stop()
|
||||
print("服务器已停止")
|
||||
except Exception as e:
|
||||
print(f"服务器运行错误: {e}")
|
||||
http_service.stop()
|
||||
|
||||
|
||||
812
unilabos/registry/devices/bioyond_workstation.yaml
Normal file
812
unilabos/registry/devices/bioyond_workstation.yaml
Normal 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
|
||||
@@ -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,108 +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.bioyond:
|
||||
workstation.example:
|
||||
category:
|
||||
- work_station
|
||||
class:
|
||||
action_value_mappings:
|
||||
auto-execute_bioyond_sync_workflow:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
parameters: null
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
parameters:
|
||||
type: object
|
||||
required:
|
||||
- parameters
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: execute_bioyond_sync_workflow参数
|
||||
type: object
|
||||
type: UniLabJsonCommandAsync
|
||||
auto-execute_bioyond_update_workflow:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
parameters: null
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
parameters:
|
||||
type: object
|
||||
required:
|
||||
- parameters
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: execute_bioyond_update_workflow参数
|
||||
type: object
|
||||
type: UniLabJsonCommandAsync
|
||||
auto-load_bioyond_data_from_file:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default:
|
||||
file_path: null
|
||||
handles: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: ''
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties:
|
||||
file_path:
|
||||
type: string
|
||||
required:
|
||||
- file_path
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: load_bioyond_data_from_file参数
|
||||
type: object
|
||||
type: UniLabJsonCommand
|
||||
module: unilabos.devices.workstation.bioyond_studio.station:BioyondWorkstation
|
||||
action_value_mappings: {}
|
||||
module: unilabos.devices.workstation.workstation_base:WorkstationExample
|
||||
status_types: {}
|
||||
type: python
|
||||
config_info: []
|
||||
description: ''
|
||||
handles: []
|
||||
icon: 反应站.webp
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
bioyond_config:
|
||||
type: string
|
||||
deck:
|
||||
type: string
|
||||
required: []
|
||||
station_resource:
|
||||
type: object
|
||||
required:
|
||||
- station_resource
|
||||
type: object
|
||||
data:
|
||||
properties: {}
|
||||
|
||||
Reference in New Issue
Block a user