8 Commits

Author SHA1 Message Date
Calvin Cao
6413828c59 Merge pull request #183 from sun7151887/yb_fix5
添加新威电池测试系统设备节点到配置文件
2025-12-02 17:06:07 +08:00
dijkstra402
5072f00836 添加新威电池测试系统设备节点到配置文件
- 在 new_cellconfig3c.json 中新增 NewareTester 设备
- 配置 IP:127.0.0.1, Port:502, Machine_ID:1
- 修复之前的 JSON 格式错误(重复对象和数组语法错误)
- 设备位置设置为 (1500, 0, 0),避免与其他设备重叠
- 包含功能说明: 720通道监控、2盘电池物料管理、CSV批量提交
2025-12-02 17:01:02 +08:00
Calvin Cao
9dfbe3246e Merge pull request #182 from sun7151887/yb_fix5
解决前端物料显示问题
2025-12-02 16:07:55 +08:00
dijkstra402
bef69db3b6 解决前端物料显示问题 2025-12-02 15:42:07 +08:00
Calvin Cao
a061bc2942 Merge pull request #181 from sun7151887/yb_fix5
修复遇到的参数错误和物料转换问题
2025-12-02 11:26:54 +08:00
dijkstra402
8c9e11c04f chore: 更新 Excel 模板文件
- 更新 2025092701.xlsx 配方文件
- 更新 material_template.xlsx 物料模板
2025-12-02 11:08:48 +08:00
dijkstra402
e4e3ec805a feat: 添加三阶段工作流函数和别名映射
- 在 BioyondCellWorkstation 添加 run_feeding_stage, run_liquid_preparation_stage, run_transfer_stage 三个阶段函数
- 在 host_node.py 添加 JSON_COMMAND_ALIASES 映射表,支持 run_feeding_stage -> auto_feeding4to3 别名
- 修复 create_orders 中 transfer_resource_to_another 参数名错误
- 简化 run_transfer_stage,注释掉物料转换逻辑,只保留核心转运功能
2025-12-02 11:05:36 +08:00
dijkstra402
d634316bce feat: enhance BioyondCellWorkstation and CoinCellAssembly workflows
- Added support for transferring resources between workstations with detailed logging.
- Introduced new methods for material conversion and resource registration.
- Updated YAML configurations to reflect new parameters and structures for workflows.
- Enhanced error handling and logging for better debugging and operational clarity.
2025-11-27 10:46:40 +08:00
9 changed files with 501 additions and 52 deletions

View File

@@ -90,9 +90,48 @@
}
},
"data": {}
},
{
"id": "NewareTester",
"name": "新威电池测试系统",
"parent": null,
"children": [],
"type": "device",
"class": "neware_battery_test_system",
"config": {
"ip": "127.0.0.1",
"port": 502,
"machine_id": 1,
"devtype": "27",
"timeout": 20,
"size_x": 500.0,
"size_y": 500.0,
"size_z": 2000.0
},
"position": {
"size": {
"height": 1600,
"width": 1200,
"depth": 800
},
"position": {
"x": 1500,
"y": 0,
"z": 0
}
},
"data": {
"功能说明": "新威电池测试系统提供720通道监控和CSV批量提交功能",
"监控功能": "支持720个通道的实时状态监控、2盘电池物料管理、状态导出等",
"提交功能": "通过submit_from_csv action从CSV文件批量提交测试任务"
}
}
],
"links": []
}

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from cgi import print_arguments
from doctest import debug
from typing import Dict, Any, List, Optional
from typing import Dict, Any, List, Optional, Tuple
import requests
from pylabrobot.resources.resource import Resource as ResourcePLR
from pathlib import Path
@@ -22,6 +22,19 @@ from unilabos.resources.bioyond.decks import BIOYOND_YB_Deck
from unilabos.resources.graphio import resource_bioyond_to_plr
from unilabos.utils.log import logger
from unilabos.registry.registry import lab_registry
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode
class device(BIOYOND_YB_Deck):
@classmethod
def deserialize(cls, data, allow_marshal=False): # type: ignore[override]
patched = dict(data)
if patched.get("type") == "device":
patched["type"] = "Deck"
if patched.get("category") == "device":
patched["category"] = "deck"
return super().deserialize(patched, allow_marshal=allow_marshal)
def _iso_local_now_ms() -> str:
# 文档要求:到毫秒 + Z例如 2025-08-15T05:43:22.814Z
@@ -45,8 +58,10 @@ class BioyondCellWorkstation(BioyondWorkstation):
**API_CONFIG,
"material_type_mappings": MATERIAL_TYPE_MAPPINGS,
"warehouse_mapping": WAREHOUSE_MAPPING,
"debug_mode": False
"debug_mode": False,
}
if config:
self.bioyond_config.update(config)
# "material_type_mappings": MATERIAL_TYPE_MAPPINGS
# "warehouse_mapping": WAREHOUSE_MAPPING
@@ -57,6 +72,12 @@ class BioyondCellWorkstation(BioyondWorkstation):
self.http_service_started = self.debug_mode
self._device_id = "bioyond_cell_workstation" # 默认值后续会从_ros_node获取
super().__init__(bioyond_config=config, deck=deck)
self.transfer_target_device_id = self.bioyond_config.get("transfer_target_device_id", "BatteryStation")
self.transfer_target_parent = self.bioyond_config.get("transfer_target_parent", "YB_YH_Deck")
self.transfer_timeout = float(self.bioyond_config.get("transfer_timeout", 180.0))
self.coin_cell_workflow_config = self.bioyond_config.get("coin_cell_workflow_config", {})
self.pending_transfer_materials: List[Dict[str, Any]] = []
self.pending_transfer_plr: List[ResourcePLR] = []
self.update_push_ip() #直接修改奔耀端的报送ip地址
logger.info("已更新奔耀端推送 IP 地址")
@@ -400,7 +421,8 @@ class BioyondCellWorkstation(BioyondWorkstation):
result = self.wait_for_order_finish(order_code)
return {
"api_response": response,
"order_finish": result
"order_finish": result,
"items": items,
}
@@ -472,7 +494,7 @@ class BioyondCellWorkstation(BioyondWorkstation):
return response
# 2.14 新建实验
def create_orders(self, xlsx_path: str) -> Dict[str, Any]:
def create_orders(self, xlsx_path: str, *, material_filter: Optional[str] = None) -> Dict[str, Any]:
"""
从 Excel 解析并创建实验2.14
约定:
@@ -629,7 +651,34 @@ class BioyondCellWorkstation(BioyondWorkstation):
return response
# 等待完成报送
result = self.wait_for_order_finish(order_code)
return result
report_data = result.get("report") if isinstance(result, dict) else None
materials_from_report = (
report_data.get("usedMaterials") if isinstance(report_data, dict) else None
)
if materials_from_report:
materials = materials_from_report
logger.info(
"[create_orders] 使用订单完成报送中的物料信息: "
f"{len(materials)}"
)
else:
materials = self._fetch_bioyond_materials(filter_keyword=material_filter)
logger.info(
"[create_orders] 未收到订单报送物料信息,回退到实时查询"
)
print("materials_from_report:", materials_from_report)
# TODO: 需要将 materials 字典转换为 ResourceSlot 对象后才能转运
# self.transfer_resource_to_another(
# resource=[materials],
# mount_resource=["YB_YH_Deck"],
# sites=[None],
# mount_device_id="BatteryStation"
# )
return {
"api_response": response,
"order_finish": result,
"materials": materials,
}
# 2.7 启动调度
def scheduler_start(self) -> Dict[str, Any]:
@@ -701,6 +750,7 @@ class BioyondCellWorkstation(BioyondWorkstation):
return response
# 等待完成报送
result = self.wait_for_order_finish(order_code)
return result
# 2.5 批量查询实验报告(post过滤关键字查询)
@@ -1198,7 +1248,109 @@ class BioyondCellWorkstation(BioyondWorkstation):
return raw_materials
def run_feeding_stage(self) -> Dict[str, List[Dict[str, Any]]]:
def _convert_materials_to_plr(self, materials: List[Dict[str, Any]]) -> List[ResourcePLR]:
try:
return resource_bioyond_to_plr(
deepcopy(materials),
type_mapping=self.bioyond_config.get("material_type_mappings", MATERIAL_TYPE_MAPPINGS),
deck=self.deck,
)
except Exception as exc:
logger.error(f"物料转换为 PLR 失败: {exc}", exc_info=True)
return []
def _wait_for_future(self, future, stage: str, timeout: Optional[float] = None):
if future is None:
return None
timeout = timeout or self.transfer_timeout
start = time.time()
while not future.done():
if (time.time() - start) > timeout:
raise TimeoutError(f"{stage} 超时 {timeout}s")
time.sleep(0.05)
return future.result()
def _register_plr_resources(self, resources: List[ResourcePLR]) -> None:
if not resources or not hasattr(self, "_ros_node") or self._ros_node is None:
return
future = ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, resources=resources)
self._wait_for_future(future, "update_resource")
def _get_target_resource(self, name: str) -> ResourcePLR:
if not hasattr(self, "_ros_node") or self._ros_node is None:
raise RuntimeError("ROS 节点未初始化,无法获取资源")
resource = self._ros_node.resource_tracker.figure_resource({"name": name}, try_mode=False) # type: ignore
if resource is None:
raise ValueError(f"未找到目标资源: {name}")
return resource
def _allocate_sites(self, parent_resource: ResourcePLR, count: int) -> List[str]:
if not hasattr(parent_resource, "get_free_sites"):
raise ValueError(f"资源 {parent_resource} 不支持自动分配站位")
free_indices = list(parent_resource.get_free_sites())
if len(free_indices) < count:
raise ValueError(f"{parent_resource.name} 可用站位不足 (need {count}, have {len(free_indices)})")
ordering = list(getattr(parent_resource, "_ordering", {}).keys())
sites: List[str] = []
for idx in free_indices[:count]:
if ordering and idx < len(ordering):
sites.append(ordering[idx])
else:
sites.append(str(idx))
return sites
def _invoke_coin_cell_workflow(self, material_payload: List[Dict[str, Any]]) -> Any:
timeout = float(self.bioyond_config.get("coin_cell_workflow_timeout", 300.0))
workflow_payload: Dict[str, Any] = {}
if isinstance(self.coin_cell_workflow_config, dict):
workflow_payload.update(deepcopy(self.coin_cell_workflow_config))
workflow_payload["materials"] = deepcopy(material_payload)
return self._call_remote_device_method(
self.transfer_target_device_id,
"run_coin_cell_assembly_workflow",
timeout=timeout,
workflow_config=workflow_payload,
)
def _call_remote_device_method(
self,
device_id: str,
method: str,
*,
timeout: Optional[float] = None,
**kwargs,
) -> Any:
if not hasattr(self, "_ros_node") or self._ros_node is None:
raise RuntimeError("ROS 节点未初始化,无法调用远程设备")
if not device_id:
raise ValueError("device_id 不能为空")
if not method:
raise ValueError("method 不能为空")
timeout = timeout or self.transfer_timeout
payload = json.dumps(
{
"function_name": method,
"function_args": kwargs,
},
ensure_ascii=False,
)
future = ROS2DeviceNode.run_async_func(
self._ros_node.execute_single_action,
True,
device_id=device_id,
action_name="_execute_driver_command_async",
action_kwargs={"string": payload},
)
result = self._wait_for_future(future, f"{device_id}.{method}", timeout)
if hasattr(result, "return_info"):
try:
return json.loads(result.return_info)
except Exception:
return result.return_info
return result
def run_feeding_stage(self) -> Dict[str, Any]:
self.create_sample(
board_type="配液瓶(小)板",
bottle_type="配液瓶(小)",
@@ -1214,11 +1366,15 @@ class BioyondCellWorkstation(BioyondWorkstation):
warehouse_name="手动堆栈"
)
self.scheduler_start()
self.auto_feeding4to3(
feeding_task = self.auto_feeding4to3(
xlsx_path="/Users/sml/work/Unilab/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx"
)
feeding_materials = self._fetch_bioyond_materials()
return {"feeding_materials": feeding_materials}
return {
"feeding_materials": feeding_materials,
"feeding_items": feeding_task.get("items", []),
"feeding_task": feeding_task,
}
def run_liquid_preparation_stage(
self,
@@ -1239,17 +1395,38 @@ class BioyondCellWorkstation(BioyondWorkstation):
def run_transfer_stage(
self,
liquid_materials: Optional[List[Dict[str, Any]]] = None,
) -> Dict[str, List[Dict[str, Any]]]:
self.transfer_3_to_2_to_1(
source_wh_id="3a19debc-84b4-0359-e2d4-b3beea49348b",
source_x=1,
source_y=1,
source_z=1,
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]:
"""转运阶段调用transfer_3_to_2_to_1执行3到2到1转运"""
logger.info("开始执行转运阶段 (run_transfer_stage)")
# 暂时注释掉物料转换和跨工站转运逻辑
# transfer_summary: Dict[str, Any] = {}
# try:
# source_materials = liquid_materials or self._fetch_bioyond_materials()
# transfer_plr = self._convert_materials_to_plr(source_materials)
# transfer_summary["plr_count"] = len(transfer_plr)
# ...
# except Exception as exc:
# transfer_summary["error"] = str(exc)
# logger.error(f"跨工站转运失败: {exc}", exc_info=True)
# 只执行核心的3到2到1转运
transfer_result = self.transfer_3_to_2_to_1(
source_wh_id=source_wh_id,
source_x=source_x,
source_y=source_y,
source_z=source_z
)
transfer_materials = self._fetch_bioyond_materials()
logger.info("转运阶段执行完成")
return {
"liquid_materials": liquid_materials or [],
"transfer_materials": transfer_materials,
"success": True,
"stage": "transfer",
"transfer_result": transfer_result
}
if __name__ == "__main__":
deck = BIOYOND_YB_Deck(setup=True)

View File

@@ -159,6 +159,27 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
"resources": [self.deck]
})
def sync_transfer_resources(self) -> Dict[str, Any]:
"""
供跨工站转运完成后调用,强制将当前台面资源同步到云端/前端。
"""
if not hasattr(self, "_ros_node") or self._ros_node is None:
return {"status": "failed", "error": "ros_node_not_ready"}
if self.deck is None:
return {"status": "failed", "error": "deck_not_initialized"}
try:
future = ROS2DeviceNode.run_async_func(
self._ros_node.update_resource,
True,
resources=[self.deck],
)
if future:
future.result()
return {"status": "success"}
except Exception as exc:
logger.error(f"同步转运资源失败: {exc}", exc_info=True)
return {"status": "failed", "error": str(exc)}
# 批量操作在这里写
async def change_hole_sheet_to_2(self, hole: MaterialHole):
hole._unilabos_state["max_sheets"] = 2
@@ -1225,26 +1246,87 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
'''
def run_coin_cell_assembly_workflow(self):
self.qiming_coin_cell_code(
fujipian_panshu=1,
fujipian_juzhendianwei=0,
gemopanshu=0,
gemo_juzhendianwei=0,
lvbodian=True,
battery_pressure_mode=True,
battery_pressure=4200,
battery_clean_ignore=False,
)
def run_coin_cell_assembly_workflow(
self,
workflow_config: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
config: Dict[str, Any]
if workflow_config is None:
config = {}
elif isinstance(workflow_config, list):
config = {"materials": workflow_config}
else:
config = workflow_config
qiming_defaults = {
"fujipian_panshu": 1,
"fujipian_juzhendianwei": 0,
"gemopanshu": 1,
"gemo_juzhendianwei": 0,
"lvbodian": True,
"battery_pressure_mode": True,
"battery_pressure": 4200,
"battery_clean_ignore": False,
}
qiming_params = {**qiming_defaults, **(config.get("qiming") or {})}
qiming_success = self.qiming_coin_cell_code(**qiming_params)
step_results: Dict[str, Any] = {}
try:
self.func_pack_device_init()
step_results["init"] = True
except Exception as exc:
step_results["init"] = f"error: {exc}"
try:
self.func_pack_device_auto()
step_results["auto"] = True
except Exception as exc:
step_results["auto"] = f"error: {exc}"
try:
self.func_pack_device_start()
self.func_pack_send_bottle_num(1)
self.func_allpack_cmd(elec_num = 1, elec_use_num = 1, elec_vol=50, assembly_type=7, assembly_pressure=4200, file_path="/Users/sml/work")
self.func_pack_send_finished_cmd()
self.func_pack_device_stop()
# 物料转换
return self
step_results["start"] = True
except Exception as exc:
step_results["start"] = f"error: {exc}"
packaging_cfg = config.get("packaging") or {}
bottle_num = packaging_cfg.get("bottle_num", 1)
try:
self.func_pack_send_bottle_num(bottle_num)
step_results["send_bottle_num"] = True
except Exception as exc:
step_results["send_bottle_num"] = f"error: {exc}"
command_defaults = {
"elec_num": 1,
"elec_use_num": 1,
"elec_vol": 50,
"assembly_type": 7,
"assembly_pressure": 4200,
"file_path": "/Users/sml/work",
}
command_params = {**command_defaults, **(packaging_cfg.get("command") or {})}
packaging_result = self.func_allpack_cmd(**command_params)
finished_result = self.func_pack_send_finished_cmd()
stop_result = self.func_pack_device_stop()
return {
"qiming": {
"params": qiming_params,
"success": qiming_success,
},
"workflow_steps": step_results,
"packaging": {
"bottle_num": bottle_num,
"command": command_params,
"result": packaging_result,
},
"finish": {
"send_finished": finished_result,
"stop": stop_result,
},
}
if __name__ == "__main__":

View File

@@ -837,6 +837,8 @@ bioyond_cell:
result:
properties:
feeding_materials:
items:
type: object
type: array
required:
- feeding_materials
@@ -876,8 +878,12 @@ bioyond_cell:
result:
properties:
feeding_materials:
items:
type: object
type: array
liquid_materials:
items:
type: object
type: array
required:
- liquid_materials
@@ -889,6 +895,8 @@ bioyond_cell:
goal:
properties:
feeding_materials:
items:
type: object
type: array
required: []
type: object
@@ -919,9 +927,15 @@ bioyond_cell:
result:
properties:
liquid_materials:
items:
type: object
type: array
transfer_materials:
items:
type: object
type: array
transfer_summary:
type: object
required:
- transfer_materials
type: object
@@ -932,10 +946,24 @@ bioyond_cell:
goal:
properties:
liquid_materials:
items:
type: object
type: array
required: []
type: object
result: {}
result:
properties:
liquid_materials:
items:
type: object
type: array
transfer_materials:
items:
type: object
type: array
transfer_summary:
type: object
type: object
required:
- goal
title: run_transfer_stage参数
@@ -1234,7 +1262,7 @@ bioyond_cell:
config_info: []
description: 配液工站
handles: []
icon: ''
icon: benyao2.webp
init_param_schema:
config:
properties:

View File

@@ -479,20 +479,139 @@ coincellassemblyworkstation_device:
type: UniLabJsonCommand
auto-run_coin_cell_assembly_workflow:
feedback: {}
goal: {}
goal_default: {}
handles: {}
goal:
properties:
workflow_config:
type: object
required: []
type: object
goal_default:
workflow_config: {}
handles:
input:
- data_key: workflow_config
data_source: handle
data_type: resource
handler_key: WorkflowConfig
label: Workflow Config
output:
- data_key: qiming
data_source: executor
data_type: resource
handler_key: QimingResult
label: Qiming Result
- data_key: workflow_steps
data_source: executor
data_type: resource
handler_key: WorkflowSteps
label: Workflow Steps
- data_key: packaging
data_source: executor
data_type: resource
handler_key: PackagingResult
label: Packaging Result
- data_key: finish
data_source: executor
data_type: resource
handler_key: FinishResult
label: Finish Result
placeholder_keys: {}
result: {}
result:
properties:
finish:
properties:
send_finished:
type: object
stop:
type: object
required:
- send_finished
- stop
type: object
packaging:
properties:
bottle_num:
type: integer
command:
type: object
result:
type: object
required:
- bottle_num
- command
- result
type: object
qiming:
properties:
params:
type: object
success:
type: boolean
required:
- params
- success
type: object
workflow_steps:
type: object
required:
- qiming
- workflow_steps
- packaging
- finish
type: object
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
properties:
workflow_config:
type: object
required: []
type: object
result: {}
result:
properties:
finish:
properties:
send_finished:
type: object
stop:
type: object
required:
- send_finished
- stop
type: object
packaging:
properties:
bottle_num:
type: integer
command:
type: object
result:
type: object
required:
- bottle_num
- command
- result
type: object
qiming:
properties:
params:
type: object
success:
type: boolean
required:
- params
- success
type: object
workflow_steps:
type: object
required:
- qiming
- workflow_steps
- packaging
- finish
type: object
required:
- goal
title: run_coin_cell_assembly_workflow参数
@@ -548,7 +667,7 @@ coincellassemblyworkstation_device:
config_info: []
description: 扣电工站
handles: []
icon: ''
icon: koudian.webp
init_param_schema:
config:
properties:

View File

@@ -674,10 +674,15 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
for loc in material.get("locations", []):
if hasattr(deck, "warehouses") and loc.get("whName") in deck.warehouses:
warehouse = deck.warehouses[loc["whName"]]
num_x = getattr(warehouse, "num_items_x", 0) or 0
num_y = getattr(warehouse, "num_items_y", 0) or 0
num_z = getattr(warehouse, "num_items_z", 0) or 0
if num_x <= 0 or num_y <= 0 or num_z <= 0:
continue
idx = (
(loc.get("y", 0) - 1) * warehouse.num_items_x * warehouse.num_items_y
+ (loc.get("x", 0) - 1) * warehouse.num_items_x
+ (loc.get("z", 0) - 1)
(loc.get("z", 0) - 1) * num_x * num_y
+ (loc.get("y", 0) - 1) * num_x
+ (loc.get("x", 0) - 1)
)
if 0 <= idx < warehouse.capacity:
if warehouse[idx] is None or isinstance(warehouse[idx], ResourceHolder):

View File

@@ -402,7 +402,6 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
return result_future.result
"""还没有改过的部分"""
def _setup_hardware_proxy(
self, device: ROS2DeviceNode, communication_device: ROS2DeviceNode, read_method, write_method