Wire bioyond/coin-cell/neware param passing and add manual-confirm CSV export

- coin_cell_assembly: align battery_info to 9 fields (Time/open_circuit_voltage/pole_weight/assembly_time/assembly_pressure/electrolyte_volume/data_coin_type/electrolyte_code/coin_cell_code); expose assembly_data single array; rename CSV column coin_num -> data_coin_type
- coin_cell_workstation.yaml: add assembly_data_output handle for auto-func_sendbottle_allpack_multi
- neware manual_confirm: accept formulations + assembly_data + csv_export_dir, unpack to parallel lists, export merged CSV to {csv_export_dir}/{date}/date_{date}.csv, output pole_weight for downstream
- neware transfer -> battery_transfer_confirm with manual_confirm node_type, timeout_seconds, assignee_user_ids
- neware test -> submit_auto_export_excel, accept pole_weight input; relabel battery_system as xml工步

Made-with: Cursor
This commit is contained in:
Xie Qiming
2026-04-21 20:01:49 +08:00
parent 52b460466d
commit d1713fcca1
4 changed files with 307 additions and 88 deletions

View File

@@ -16,10 +16,12 @@
import os
import sys
import socket
import csv
import xml.etree.ElementTree as ET
import json
import time
import inspect
from datetime import datetime
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, TypedDict
@@ -1941,34 +1943,191 @@ class NewareBatteryTestSystem:
active_material: List[float],
capacity: List[float],
battery_system: List[str],
timeout_seconds: int,
assignee_user_ids: list[str],
**kwargs
formulations: List[Dict] = None,
assembly_data: List[Dict] = None,
csv_export_dir: str = "D:\\2604Agentic_test",
timeout_seconds: int = 3600,
assignee_user_ids: list[str] = None,
**kwargs,
) -> dict:
"""
timeout_seconds: 超时时间默认3600秒
collector_mass: 极流体质量
active_material: 活性物质含量
capacity: 克容量mAh/g
battery_system: 电池体系
修改的结果无效,是只读的
人工确认节点:
- 上游接收 bioyond 配方formulations+ 扣电组装数据assembly_data 单数组)
- 人工在前端填入 collector_mass / active_material / capacity / battery_system(xml工步)
并选择 target_device 与 mount_resource通道
- 内部把 assembly_data 解包为 9 个并行数组,把 pole_weight 透传给下游 submit_auto_export_excel
- 把所有数据整合后写入 {csv_export_dir}/{YYYYMMDD}/date_{YYYYMMDD}.csv
Args:
timeout_seconds: 超时时间(秒),默认 3600
collector_mass: 极流体质量 (mg)
active_material: 活性物质含量 (0.97 或 "97%")
capacity: 克容量 (mAh/g)
battery_system: xml 工步标识(如 "811_LI_002"
formulations: 配方信息列表(来自 bioyond mass_ratios
assembly_data: 扣电组装数据列表(每颗电池一个 dict
csv_export_dir: 整合 CSV 导出根目录
"""
resource = ResourceTreeSet.from_plr_resources(resource).dump()
mount_resource = ResourceTreeSet.from_plr_resources(mount_resource).dump()
kwargs.update(locals())
kwargs.pop("kwargs")
kwargs.pop("self")
return kwargs
resource_dump = ResourceTreeSet.from_plr_resources(resource).dump()
mount_resource_dump = ResourceTreeSet.from_plr_resources(mount_resource).dump()
assembly_data = assembly_data or []
formulations = formulations or []
Time = [b.get("Time", "") for b in assembly_data]
open_circuit_voltage = [b.get("open_circuit_voltage", 0.0) for b in assembly_data]
pole_weight = [b.get("pole_weight", 0.0) for b in assembly_data]
assembly_time = [b.get("assembly_time", 0) for b in assembly_data]
assembly_pressure = [b.get("assembly_pressure", 0) for b in assembly_data]
electrolyte_volume = [b.get("electrolyte_volume", 0) for b in assembly_data]
data_coin_type = [b.get("data_coin_type", 0) for b in assembly_data]
electrolyte_code = [b.get("electrolyte_code", "") for b in assembly_data]
coin_cell_code = [b.get("coin_cell_code", "") for b in assembly_data]
try:
self._export_manual_confirm_csv(
csv_export_dir=csv_export_dir,
mount_resource=mount_resource,
formulations=formulations,
assembly_rows={
"Time": Time,
"open_circuit_voltage": open_circuit_voltage,
"pole_weight": pole_weight,
"assembly_time": assembly_time,
"assembly_pressure": assembly_pressure,
"electrolyte_volume": electrolyte_volume,
"data_coin_type": data_coin_type,
"electrolyte_code": electrolyte_code,
"coin_cell_code": coin_cell_code,
},
collector_mass=collector_mass,
active_material=active_material,
capacity=capacity,
battery_system=battery_system,
)
except Exception as e:
if self._ros_node:
self._ros_node.lab_logger().warning(f"[manual_confirm] 整合 CSV 导出失败: {e}")
else:
print(f"[manual_confirm] 整合 CSV 导出失败: {e}")
return {
"resource": resource_dump,
"target_device": target_device,
"mount_resource": mount_resource_dump,
"collector_mass": collector_mass,
"active_material": active_material,
"capacity": capacity,
"battery_system": battery_system,
"formulations": formulations,
"assembly_data": assembly_data,
"pole_weight": pole_weight,
}
def _export_manual_confirm_csv(
self,
csv_export_dir: str,
mount_resource: List[ResourceSlot],
formulations: List[Dict],
assembly_rows: Dict[str, List[Any]],
collector_mass: List[float],
active_material: List[float],
capacity: List[float],
battery_system: List[str],
) -> Optional[str]:
"""把 manual_confirm 收集到的全部参数整合写入 CSV。路径{csv_export_dir}/{YYYYMMDD}/date_{YYYYMMDD}.csv"""
n_assembly = len(assembly_rows.get("Time", []))
n_channel = len(mount_resource) if mount_resource else 0
n = max(n_assembly, n_channel, len(collector_mass or []), len(active_material or []),
len(capacity or []), len(battery_system or []))
if n == 0:
return None
date_str = datetime.now().strftime("%Y%m%d")
out_dir = os.path.join(csv_export_dir, date_str)
os.makedirs(out_dir, exist_ok=True)
out_path = os.path.join(out_dir, f"date_{date_str}.csv")
header = [
"Time", "open_circuit_voltage", "pole_weight",
"assembly_time", "assembly_pressure", "electrolyte_volume",
"data_coin_type", "electrolyte_code", "coin_cell_code",
"orderName", "prep_bottle_barcode", "vial_bottle_barcodes",
"target_mass_ratio", "real_mass_ratio",
"collector_mass", "active_material", "capacity", "battery_system",
"channel_name",
]
file_exists = os.path.exists(out_path)
with open(out_path, "a", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
if not file_exists:
writer.writerow(header)
def safe_get(lst, i, default=""):
try:
return lst[i] if lst and i < len(lst) else default
except Exception:
return default
for i in range(n):
form = formulations[i] if formulations and i < len(formulations) else {}
target_ratio = form.get("target_mass_ratio", {}) if isinstance(form, dict) else {}
real_ratio = form.get("real_mass_ratio", {}) if isinstance(form, dict) else {}
ch_name = self._extract_channel_name(mount_resource[i]) if mount_resource and i < len(mount_resource) else ""
writer.writerow([
safe_get(assembly_rows["Time"], i),
safe_get(assembly_rows["open_circuit_voltage"], i, 0.0),
safe_get(assembly_rows["pole_weight"], i, 0.0),
safe_get(assembly_rows["assembly_time"], i, 0),
safe_get(assembly_rows["assembly_pressure"], i, 0),
safe_get(assembly_rows["electrolyte_volume"], i, 0),
safe_get(assembly_rows["data_coin_type"], i, 0),
safe_get(assembly_rows["electrolyte_code"], i),
safe_get(assembly_rows["coin_cell_code"], i),
form.get("orderName", "") if isinstance(form, dict) else "",
form.get("prep_bottle_barcode", "") if isinstance(form, dict) else "",
form.get("vial_bottle_barcodes", "") if isinstance(form, dict) else "",
json.dumps(target_ratio, ensure_ascii=False) if target_ratio else "",
json.dumps(real_ratio, ensure_ascii=False) if real_ratio else "",
safe_get(collector_mass, i, ""),
safe_get(active_material, i, ""),
safe_get(capacity, i, ""),
safe_get(battery_system, i, ""),
ch_name or "",
])
f.flush()
if self._ros_node:
self._ros_node.lab_logger().info(f"[manual_confirm] 整合 CSV 已写入 {out_path}{n} 行)")
return out_path
async def transfer(self, resource: List[ResourceSlot], target_device: DeviceSlot, mount_resource: List[ResourceSlot]):
future = ROS2DeviceNode.run_async_func(self._ros_node.transfer_resource_to_another, True,
async def battery_transfer_confirm(
self,
resource: List[ResourceSlot],
target_device: DeviceSlot,
mount_resource: List[ResourceSlot],
timeout_seconds: int = 3600,
assignee_user_ids: list[str] = None,
**kwargs,
):
"""
电池装夹人工确认 + TCP 转运。
- 该节点通过 yaml 的 node_type: manual_confirm 机制阻塞等待人工确认。
- 人工在前端确认通道与电池对应关系(装夹就位)后,方法体才会被框架调用。
- 方法体执行真正的 TCP 资源转运。
"""
future = ROS2DeviceNode.run_async_func(
self._ros_node.transfer_resource_to_another, True,
**{
"plr_resources": resource,
"target_device_id": target_device,
"target_resources": mount_resource,
"sites": [None] * len(mount_resource),
})
},
)
result = await future
return result
# ──────────────────────────────────────────────
@@ -2043,7 +2202,7 @@ class NewareBatteryTestSystem:
# test 动作:下发测试
# ──────────────────────────────────────────────
async def test(
async def submit_auto_export_excel(
self,
resource: List[ResourceSlot],
mount_resource: List[ResourceSlot],
@@ -2051,17 +2210,19 @@ class NewareBatteryTestSystem:
active_material: List[float],
capacity: List[float],
battery_system: List[str],
pole_weight: List[float] = None,
) -> dict:
"""
对每颗电池计算测试参数、生成 XML 工步文件并通过 TCP 下发给新威测试仪。
Args:
resource: 成品电池资源列表(含 pole_weight 状态)
resource: 成品电池资源列表(含 pole_weight 状态,仅当 pole_weight 入参为空时作为回退
mount_resource: 目标通道资源列表(含 Channel_Name = devid-subdevid-chlid
collector_mass: 各电池集流体质量 (mg)
active_material: 各电池活性物质比例0.97 或 "97%"
capacity: 各电池克容量 (mAh/g)
battery_system: 各电池体系名称(如 "811_LI_002"
battery_system: xml 工步标识(如 "811_LI_002"
pole_weight: 各电池极片质量 (mg),来自上游 manual_confirm 的透传;为 None 时回退到从 resource 状态提取
"""
import importlib
gen_mod = importlib.import_module(
@@ -2096,7 +2257,10 @@ class NewareBatteryTestSystem:
or (res.get("name") if isinstance(res, dict) else None)
or f"battery_{i}"
)
pw = self._extract_pole_weight(res)
if pole_weight and i < len(pole_weight):
pw = float(pole_weight[i])
else:
pw = self._extract_pole_weight(res)
# 3. 计算活性物质质量与容量
cm = float(collector_mass[i])

View File

@@ -1650,6 +1650,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
time_date = datetime.now().strftime("%Y%m%d")
#秒级时间戳用于标记每一行电池数据
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
self._last_assembly_timestamp = timestamp
#生成输出文件的变量
self.csv_export_file = os.path.join(file_path, f"date_{time_date}.csv")
#将数据存入csv文件
@@ -1660,7 +1661,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
writer.writerow([
'Time', 'open_circuit_voltage', 'pole_weight',
'assembly_time', 'assembly_pressure', 'electrolyte_volume',
'coin_num', 'electrolyte_code', 'coin_cell_code',
'data_coin_type', 'electrolyte_code', 'coin_cell_code',
'orderName', 'prep_bottle_barcode', 'vial_bottle_barcodes',
'target_mass_ratio', 'real_mass_ratio'
])
@@ -1877,17 +1878,18 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
pole_weight = 0.0
battery_info = {
"battery_index": coin_num_N + 1,
"battery_barcode": battery_qr_code,
"electrolyte_barcode": electrolyte_qr_code,
"Time": getattr(self, "_last_assembly_timestamp", datetime.now().strftime("%Y%m%d_%H%M%S")),
"open_circuit_voltage": open_circuit_voltage,
"pole_weight": pole_weight,
"assembly_time": self.data_assembly_time,
"assembly_pressure": self.data_assembly_pressure,
"electrolyte_volume": self.data_electrolyte_volume
"electrolyte_volume": self.data_electrolyte_volume,
"data_coin_type": getattr(self, "data_coin_type", 0),
"electrolyte_code": electrolyte_qr_code,
"coin_cell_code": battery_qr_code,
}
battery_data_list.append(battery_info)
print(f"已收集第 {coin_num_N + 1} 个电池数据: 电池码={battery_info['battery_barcode']}, 电解液码={battery_info['electrolyte_barcode']}")
print(f"已收集第 {coin_num_N + 1} 个电池数据: 电池码={battery_info['coin_cell_code']}, 电解液码={battery_info['electrolyte_code']}")
time.sleep(1)
# TODO:读完再将电池数加一还是进入循环就将电池数加一需要考虑
@@ -1916,6 +1918,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
"success": True,
"total_batteries": len(battery_data_list),
"batteries": battery_data_list,
"assembly_data": battery_data_list,
"summary": {
"electrolyte_bottles_used": elec_num,
"batteries_per_bottle": elec_use_num,
@@ -2130,17 +2133,18 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
pole_weight = 0.0
battery_info = {
"battery_index": coin_num_N + 1,
"battery_barcode": battery_qr_code,
"electrolyte_barcode": electrolyte_qr_code,
"Time": getattr(self, "_last_assembly_timestamp", datetime.now().strftime("%Y%m%d_%H%M%S")),
"open_circuit_voltage": open_circuit_voltage,
"pole_weight": pole_weight,
"assembly_time": self.data_assembly_time,
"assembly_pressure": self.data_assembly_pressure,
"electrolyte_volume": self.data_electrolyte_volume
"electrolyte_volume": self.data_electrolyte_volume,
"data_coin_type": getattr(self, "data_coin_type", 0),
"electrolyte_code": electrolyte_qr_code,
"coin_cell_code": battery_qr_code,
}
battery_data_list.append(battery_info)
print(f"已收集第 {coin_num_N + 1} 个电池数据: 电池码={battery_info['battery_barcode']}, 电解液码={battery_info['electrolyte_barcode']}")
print(f"已收集第 {coin_num_N + 1} 个电池数据: 电池码={battery_info['coin_cell_code']}, 电解液码={battery_info['electrolyte_code']}")
time.sleep(1)
@@ -2167,6 +2171,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
"success": True,
"total_batteries": len(battery_data_list),
"batteries": battery_data_list,
"assembly_data": battery_data_list,
"summary": {
"electrolyte_bottles_used": elec_num,
"batteries_per_bottle": elec_use_num,

View File

@@ -486,6 +486,12 @@ coincellassemblyworkstation_device:
data_type: array
handler_key: formulations_input
label: 配方信息列表
output:
- data_key: assembly_data
data_source: executor
data_type: array
handler_key: assembly_data_output
label: 扣电组装数据列表
placeholder_keys: {}
result: {}
schema:

View File

@@ -364,6 +364,9 @@ neware_battery_test_system:
active_material: active_material
capacity: capacity
battery_system: battery_system
formulations: formulations
assembly_data: assembly_data
csv_export_dir: csv_export_dir
timeout_seconds: timeout_seconds
assignee_user_ids: assignee_user_ids
feedback: {}
@@ -375,6 +378,9 @@ neware_battery_test_system:
active_material: active_material
capacity: capacity
battery_system: battery_system
formulations: formulations
assembly_data: assembly_data
pole_weight: pole_weight
schema:
title: manual_confirm参数
description: manual_confirm的参数schema
@@ -570,6 +576,20 @@ neware_battery_test_system:
type: array
items:
type: string
formulations:
type: array
description: 配方信息列表(来自 bioyond create_orders_formulation 的 mass_ratios 输出)
items:
type: object
assembly_data:
type: array
description: 扣电组装数据列表(每颗电池一个对象,含 Time/open_circuit_voltage/pole_weight 等 9 字段)
items:
type: object
csv_export_dir:
type: string
default: 'D:\2604Agentic_test'
description: 整合 CSV 导出根目录(按日期子目录分组)
timeout_seconds:
type: integer
assignee_user_ids:
@@ -577,13 +597,6 @@ neware_battery_test_system:
items:
type: string
required:
- resource
- target_device
- mount_resource
- collector_mass
- active_material
- capacity
- battery_system
- timeout_seconds
- assignee_user_ids
_unilabos_placeholder_info:
@@ -604,50 +617,23 @@ neware_battery_test_system:
active_material: []
capacity: []
battery_system: []
formulations: []
assembly_data: []
csv_export_dir: 'D:\2604Agentic_test'
timeout_seconds: 3600
assignee_user_ids: []
handles:
input:
- handler_key: target_device
data_type: device_id
label: 目标设备
data_key: target_device
- handler_key: formulations
data_type: array
label: 配方信息列表
data_key: formulations
data_source: handle
io_type: source
- handler_key: resource
data_type: resource
label: 待转移资源
data_key: resource
data_source: handle
io_type: source
- handler_key: mount_resource
data_type: resource
label: 目标孔位
data_key: mount_resource
data_source: handle
io_type: source
- handler_key: collector_mass
data_type: collector_mass
label: 极流体质量
data_key: collector_mass
data_source: handle
io_type: source
- handler_key: active_material
data_type: active_material
label: 活性物质含量
data_key: active_material
data_source: handle
io_type: source
- handler_key: capacity
data_type: capacity
label: 克容量
data_key: capacity
data_source: handle
io_type: source
- handler_key: battery_system
data_type: battery_system
label: 电池体系
data_key: battery_system
- handler_key: assembly_data
data_type: array
label: 扣电组装数据列表
data_key: assembly_data
data_source: handle
io_type: source
output:
@@ -683,9 +669,24 @@ neware_battery_test_system:
data_source: executor
- handler_key: battery_system
data_type: battery_system
label: 电池体系
label: xml工步
data_key: battery_system
data_source: executor
- handler_key: pole_weight
data_type: array
label: 极片质量
data_key: pole_weight
data_source: executor
- handler_key: formulations
data_type: array
label: 配方信息列表
data_key: formulations
data_source: executor
- handler_key: assembly_data
data_type: array
label: 扣电组装数据列表
data_key: assembly_data
data_source: executor
placeholder_keys:
resource: unilabos_resources
target_device: unilabos_devices
@@ -694,7 +695,7 @@ neware_battery_test_system:
always_free: true
feedback_interval: 300
node_type: manual_confirm
test:
submit_auto_export_excel:
type: UniLabJsonCommandAsync
goal:
resource: resource
@@ -703,6 +704,7 @@ neware_battery_test_system:
active_material: active_material
capacity: capacity
battery_system: battery_system
pole_weight: pole_weight
feedback: {}
result:
return_info: return_info
@@ -711,8 +713,8 @@ neware_battery_test_system:
total_count: total_count
results: results
schema:
title: test参数
description: test的参数schema
title: submit_auto_export_excel参数
description: submit_auto_export_excel的参数schema
type: object
properties:
goal:
@@ -902,6 +904,10 @@ neware_battery_test_system:
type: array
items:
type: string
pole_weight:
type: array
items:
type: number
required:
- resource
- mount_resource
@@ -923,6 +929,7 @@ neware_battery_test_system:
active_material: []
capacity: []
battery_system: []
pole_weight: []
handles:
input:
- handler_key: resource
@@ -957,26 +964,34 @@ neware_battery_test_system:
io_type: source
- handler_key: battery_system
data_type: battery_system
label: 电池体系
label: xml工步
data_key: battery_system
data_source: handle
io_type: source
- handler_key: pole_weight
data_type: array
label: 极片质量
data_key: pole_weight
data_source: handle
io_type: source
output: []
placeholder_keys:
resource: unilabos_resources
mount_resource: unilabos_resources
feedback_interval: 1.0
transfer:
battery_transfer_confirm:
type: UniLabJsonCommandAsync
goal:
resource: resource
target_device: target_device
mount_resource: mount_resource
timeout_seconds: timeout_seconds
assignee_user_ids: assignee_user_ids
feedback: {}
result: {}
schema:
title: transfer参数
description: transfer的参数schema
title: battery_transfer_confirm参数
description: battery_transfer_confirm的参数schema
type: object
properties:
goal:
@@ -1153,14 +1168,23 @@ neware_battery_test_system:
type: string
title: mount_resource
type: array
timeout_seconds:
type: integer
assignee_user_ids:
type: array
items:
type: string
required:
- resource
- target_device
- mount_resource
- timeout_seconds
- assignee_user_ids
_unilabos_placeholder_info:
resource: unilabos_resources
target_device: unilabos_devices
mount_resource: unilabos_resources
assignee_user_ids: unilabos_manual_confirm
feedback: {}
result: {}
required:
@@ -1169,6 +1193,8 @@ neware_battery_test_system:
resource: []
target_device: ''
mount_resource: []
timeout_seconds: 3600
assignee_user_ids: []
handles:
input:
- handler_key: target_device
@@ -1189,12 +1215,30 @@ neware_battery_test_system:
data_key: mount_resource
data_source: handle
io_type: source
output: []
output:
- handler_key: target_device
data_type: device_id
label: 目标设备
data_key: target_device
data_source: executor
- handler_key: resource
data_type: resource
label: 待转移资源
data_key: resource.@flatten
data_source: executor
- handler_key: mount_resource
data_type: resource
label: 目标孔位
data_key: mount_resource.@flatten
data_source: executor
placeholder_keys:
resource: unilabos_resources
target_device: unilabos_devices
mount_resource: unilabos_resources
feedback_interval: 1.0
assignee_user_ids: unilabos_manual_confirm
always_free: true
feedback_interval: 300
node_type: manual_confirm
module: unilabos.devices.neware_battery_test_system.neware_battery_test_system:NewareBatteryTestSystem
status_types:
channel_status: Dict[int, Dict]