4 Commits

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -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()

View File

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

View File

@@ -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: {}