diff --git a/unilabos/devices/neware_battery_test_system/device.json b/unilabos/devices/neware_battery_test_system/device.json
index 696112de..9cba5800 100644
--- a/unilabos/devices/neware_battery_test_system/device.json
+++ b/unilabos/devices/neware_battery_test_system/device.json
@@ -14,7 +14,7 @@
"config": {
"ip": "127.0.0.1",
"port": 502,
- "machine_id": 1,
+ "machine_ids": [1, 2, 3, 4, 5, 6, 86],
"devtype": "27",
"timeout": 20,
"size_x": 500.0,
@@ -32,4 +32,4 @@
}
],
"links": []
-}
\ No newline at end of file
+}
diff --git a/unilabos/devices/neware_battery_test_system/generate_xml_content.py b/unilabos/devices/neware_battery_test_system/generate_xml_content.py
new file mode 100644
index 00000000..50a8b293
--- /dev/null
+++ b/unilabos/devices/neware_battery_test_system/generate_xml_content.py
@@ -0,0 +1,1361 @@
+def xml_811_Li_002(act_mass, Cap_mAh):
+ """
+ 生成XML内容
+
+ 参数:
+ act_mass: 正极质量(mg)
+ Cap_mAh: 正极载量(mAh)
+ devid: 设备号
+ subdevid: 排号
+ chlid: 通道号
+ """
+ xml_data = f"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """
+ return xml_data
+
+def xml_811_Li_005(act_mass, Cap_mAh):
+ """
+ 生成XML内容
+
+ 参数:
+ act_mass: 正极质量(mg)
+ Cap_mAh: 正极载量(mAh)
+ devid: 设备号
+ subdevid: 排号
+ chlid: 通道号
+ """
+ xml_data = f"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """
+ return xml_data
+
+def xml_LFP_Li(act_mass, Cap_mAh):
+ """
+ 生成XML内容
+
+ 参数:
+ act_mass: 正极质量(mg)
+ Cap_mAh: 正极载量(mAh)
+ devid: 设备号
+ subdevid: 排号
+ chlid: 通道号
+ """
+ xml_data = f"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """
+ return xml_data
+
+def xml_LFP_Gr(act_mass, Cap_mAh):
+ """
+ 生成XML内容
+
+ 参数:
+ act_mass: 正极质量(mg)
+ Cap_mAh: 正极载量(mAh)
+ devid: 设备号
+ subdevid: 排号
+ chlid: 通道号
+ """
+ xml_data = f"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """
+ return xml_data
+
+def xml_Gr_Li(act_mass, Cap_mAh):
+ """
+ 生成XML内容
+
+ 参数:
+ act_mass: 正极质量(mg)
+ Cap_mAh: 正极载量(mAh)
+ devid: 设备号
+ subdevid: 排号
+ chlid: 通道号
+ """
+ xml_data = f"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """
+ return xml_data
+
+def xml_LB6(act_mass, Cap_mAh):
+ """
+ 生成XML内容
+
+ 参数:
+ act_mass: 正极质量(mg)
+ Cap_mAh: 正极载量(mAh)
+ devid: 设备号
+ subdevid: 排号
+ chlid: 通道号
+ """
+ xml_data = f"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """
+ return xml_data
+
+
+def xml_SiGr_Li_Step(act_mass, Cap_mAh):
+ """
+ 生成XML内容
+
+ 参数:
+ act_mass: 正极质量(mg)
+ Cap_mAh: 正极载量(mAh)
+ devid: 设备号
+ subdevid: 排号
+ chlid: 通道号
+ """
+ xml_data= f"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """
+ return xml_data
+
+def xml_811_SiGr(act_mass, Cap_mAh):
+ """
+ 生成XML内容
+
+ 参数:
+ act_mass: 正极质量(mg)
+ Cap_mAh: 正极载量(mAh)
+ devid: 设备号
+ subdevid: 排号
+ chlid: 通道号
+ """
+ xml_data= f"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """
+ return xml_data
+
+def xml_811_Cu_aging(act_mass, Cap_mAh):
+ """
+ 生成XML内容
+
+ 参数:
+ act_mass: 正极质量(mg)
+ Cap_mAh: 正极载量(mAh)
+ devid: 设备号
+ subdevid: 排号
+ chlid: 通道号
+ """
+ xml_data= f"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """
+ return xml_data
+def xml_ZQXNLRMO(act_mass, Cap_mAh):
+ """
+ 生成XML内容
+
+ 参数:
+ act_mass: 正极质量(mg)
+ Cap_mAh: 正极载量(mAh)
+ devid: 设备号
+ subdevid: 排号
+ chlid: 通道号
+ """
+ xml_data = f"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """
+ return xml_data
\ No newline at end of file
diff --git a/unilabos/devices/neware_battery_test_system/neware_battery_test_system.py b/unilabos/devices/neware_battery_test_system/neware_battery_test_system.py
index 0a811458..8950e677 100644
--- a/unilabos/devices/neware_battery_test_system/neware_battery_test_system.py
+++ b/unilabos/devices/neware_battery_test_system/neware_battery_test_system.py
@@ -305,11 +305,12 @@ class NewareBatteryTestSystem:
ascii_lowercase = 'abcdefghijklmnopqrstuvwxyz'
ascii_uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
LETTERS = ascii_uppercase + ascii_lowercase
+ DEFAULT_MACHINE_IDS = [1, 2, 3, 4, 5, 6, 86]
def __init__(self,
ip: str = None,
port: int = None,
- machine_id: int = 1,
+ machine_ids: Optional[List[int]] = None,
devtype: str = None,
timeout: int = None,
@@ -326,16 +327,18 @@ class NewareBatteryTestSystem:
Args:
ip: TCP服务器IP地址
port: TCP端口
+ machine_ids: 设备ID列表
devtype: 设备类型标识
timeout: 通信超时时间(秒)
- machine_id: 机器ID
size_x, size_y, size_z: 设备物理尺寸
oss_upload_enabled: 是否启用OSS上传功能,默认False
oss_prefix: OSS对象路径前缀,默认"neware_backup"
"""
self.ip = ip or self.BTS_IP
self.port = port or self.BTS_PORT
- self.machine_id = machine_id
+ self.machine_ids = machine_ids
+ self.display_device_ids = self._resolve_display_device_ids()
+ self.primary_device_id = self.display_device_ids[0]
self.devtype = devtype or self.DEVTYPE
self.timeout = timeout or self.TIMEOUT
@@ -352,6 +355,12 @@ class NewareBatteryTestSystem:
self._cached_status = {}
self._last_backup_dir = None # 记录最近一次的 backup_dir,供上传使用
self._ros_node: Optional[ROS2WorkstationNode] = None # ROS节点引用,由框架设置
+ self._channels = self._build_channel_map()
+
+ def _resolve_display_device_ids(self) -> List[int]:
+ if self.machine_ids:
+ return [int(devid) for devid in self.machine_ids]
+ return self.DEFAULT_MACHINE_IDS.copy()
def post_init(self, ros_node):
@@ -376,27 +385,72 @@ class NewareBatteryTestSystem:
ros_node.lab_logger().error(f"新威电池测试系统初始化失败: {e}")
# 不抛出异常,允许节点继续运行,后续可以重试连接
+ def _plate_name(self, devid: int, plate_num: int) -> str:
+ return f"{devid}_P{plate_num}"
+
+ def _plate_resource_key(self, devid: int, plate_num: int, row_idx: int, col_idx: int) -> str:
+ return f"{self._plate_name(devid, plate_num)}_{self.LETTERS[row_idx]}{col_idx + 1}"
+
+ def _get_plate_resource(self, devid: int, plate_num: int, row_idx: int, col_idx: int):
+ possible_names = [
+ f"{self._plate_name(devid, plate_num)}_batterytestposition_{col_idx}_{row_idx}",
+ f"{self._plate_name(devid, plate_num)}_{self.LETTERS[row_idx]}{col_idx + 1}",
+ f"{self._plate_name(devid, plate_num)}_{self.LETTERS[row_idx].lower()}{col_idx + 1}",
+ f"P{plate_num}_batterytestposition_{col_idx}_{row_idx}",
+ f"P{plate_num}_{self.LETTERS[row_idx]}{col_idx + 1}",
+ f"P{plate_num}_{self.LETTERS[row_idx].lower()}{col_idx + 1}",
+ ]
+ for name in possible_names:
+ if name in self.station_resources:
+ return self.station_resources[name], name, possible_names
+ return None, None, possible_names
+
def _setup_material_management(self):
"""设置物料管理系统"""
- # 第1盘:5行8列网格 (A1-E8) - 5行对应subdevid 1-5,8列对应chlid 1-8
- # 先给物料设置一个最大的Deck,并设置其在空间中的位置
-
- deck_main = Deck("ADeckName", 2000, 1800, 100, origin=Coordinate(2000,2000,0))
-
- plate1_resources: Dict[str, BatteryTestPosition] = create_ordered_items_2d(
- BatteryTestPosition,
- num_items_x=8, # 8列(对应chlid 1-8)
- num_items_y=5, # 5行(对应subdevid 1-5,即A-E)
- dx=10,
- dy=10,
- dz=0,
- item_dx=65,
- item_dy=65
+ deck_main = Deck(
+ name="ADeckName",
+ size_x=2200,
+ size_y=2800,
+ size_z=100,
+ origin=Coordinate(2000, 2000, 0)
)
- plate1 = Plate("P1", 400, 300, 50, ordered_items=plate1_resources)
- deck_main.assign_child_resource(plate1, location=Coordinate(0, 0, 0))
-
- # 只有在真实ROS环境下才调用update_resource
+ self.station_resources = {}
+ self.station_resources_by_plate = {}
+
+ for row_idx, devid in enumerate(self.display_device_ids):
+ for plate_num in (1, 2):
+ plate_resources: Dict[str, BatteryTestPosition] = create_ordered_items_2d(
+ BatteryTestPosition,
+ num_items_x=8,
+ num_items_y=5,
+ dx=10,
+ dy=10,
+ dz=0,
+ item_dx=65,
+ item_dy=65
+ )
+ plate_name = self._plate_name(devid, plate_num)
+ plate = Plate(
+ name=plate_name,
+ size_x=400,
+ size_y=300,
+ size_z=50,
+ ordered_items=plate_resources
+ )
+ location_x = 0 if plate_num == 1 else 450
+ location_y = row_idx * 350
+ deck_main.assign_child_resource(plate, location=Coordinate(location_x, location_y, 0))
+
+ plate_key = (devid, plate_num)
+ self.station_resources_by_plate[plate_key] = {}
+ for name, resource in plate_resources.items():
+ new_name = f"{plate_name}_{name}"
+ self.station_resources_by_plate[plate_key][new_name] = resource
+ self.station_resources[new_name] = resource
+
+ self.station_resources_plate1 = self.station_resources_by_plate.get((self.primary_device_id, 1), {})
+ self.station_resources_plate2 = self.station_resources_by_plate.get((self.primary_device_id, 2), {})
+
if hasattr(self._ros_node, 'update_resource') and callable(getattr(self._ros_node, 'update_resource')):
try:
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
@@ -405,40 +459,6 @@ class NewareBatteryTestSystem:
except Exception as e:
if hasattr(self._ros_node, 'lab_logger'):
self._ros_node.lab_logger().warning(f"更新资源失败: {e}")
- # 在非ROS环境下忽略此错误
-
- # 为第1盘资源添加P1_前缀
- self.station_resources_plate1 = {}
- for name, resource in plate1_resources.items():
- new_name = f"P1_{name}"
- self.station_resources_plate1[new_name] = resource
-
- # 第2盘:5行8列网格 (A1-E8),在Z轴上偏移 - 5行对应subdevid 6-10,8列对应chlid 1-8
- plate2_resources = create_ordered_items_2d(
- BatteryTestPosition,
- num_items_x=8, # 8列(对应chlid 1-8)
- num_items_y=5, # 5行(对应subdevid 6-10,即A-E)
- dx=10,
- dy=10,
- dz=0,
- item_dx=65,
- item_dy=65
- )
-
- plate2 = Plate("P2", 400, 300, 50, ordered_items=plate2_resources)
- deck_main.assign_child_resource(plate2, location=Coordinate(0, 350, 0))
-
-
- # 为第2盘资源添加P2_前缀
- self.station_resources_plate2 = {}
- for name, resource in plate2_resources.items():
- new_name = f"P2_{name}"
- self.station_resources_plate2[new_name] = resource
-
- # 合并两盘资源为统一的station_resources
- self.station_resources = {}
- self.station_resources.update(self.station_resources_plate1)
- self.station_resources.update(self.station_resources_plate2)
# ========================
# 核心属性(Uni-Lab标准)
@@ -469,16 +489,16 @@ class NewareBatteryTestSystem:
status_map = self._query_all_channels()
status_processed = {} if not status_map else self._group_by_devid(status_map)
- # 修复数据过滤逻辑:如果machine_id对应的数据不存在,尝试使用第一个可用的设备数据
- status_current_machine = status_processed.get(self.machine_id, {})
+ # 返回主设备数据,如果主设备没有匹配数据则回退到首个可用设备
+ status_current_machine = status_processed.get(self.primary_device_id, {})
if not status_current_machine and status_processed:
- # 如果machine_id没有匹配到数据,使用第一个可用的设备数据
+ # 如果主设备没有匹配到数据,使用第一个可用的设备数据
first_devid = next(iter(status_processed.keys()))
status_current_machine = status_processed[first_devid]
if self._ros_node:
self._ros_node.lab_logger().warning(
- f"machine_id {self.machine_id} 没有匹配到数据,使用设备ID {first_devid} 的数据"
+ f"主设备ID {self.primary_device_id} 没有匹配到数据,使用设备ID {first_devid} 的数据"
)
# 确保有默认的数据结构
@@ -488,139 +508,57 @@ class NewareBatteryTestSystem:
"subunits": {}
}
- # 确保subunits存在
- subunits = status_current_machine.get("subunits", {})
-
- # 处理2盘电池的状态映射
- self._update_plate_resources(subunits)
+ self._update_plate_resources(status_processed)
return status_current_machine
- def _update_plate_resources(self, subunits: Dict):
- """更新两盘电池资源的状态"""
- # 第1盘:subdevid 1-5 映射到 8列5行网格 (列0-7, 行0-4)
- for subdev_id in range(1, 6): # subdevid 1-5
- status_row = subunits.get(subdev_id, {})
-
- for chl_id in range(1, 9): # chlid 1-8
- try:
- # 根据用户描述:第一个是(0,0),最后一个是(7,4)
- # 说明是8列5行,列从0开始,行从0开始
- col_idx = (chl_id - 1) # 0-7 (chlid 1-8 -> 列0-7)
- row_idx = (subdev_id - 1) # 0-4 (subdevid 1-5 -> 行0-4)
-
- # 尝试多种可能的资源命名格式
- possible_names = [
- f"P1_batterytestposition_{col_idx}_{row_idx}", # 用户提到的格式
- f"P1_{self.LETTERS[row_idx]}{col_idx + 1}", # 原有的A1-E8格式
- f"P1_{self.LETTERS[row_idx].lower()}{col_idx + 1}", # 小写字母格式
- ]
-
- r = None
- resource_name = None
- for name in possible_names:
- if name in self.station_resources:
- r = self.station_resources[name]
- resource_name = name
- break
-
- if r:
- status_channel = status_row.get(chl_id, {})
- metrics = status_channel.get("metrics", {})
- # 构建BatteryTestPosition状态数据(移除capacity和energy)
- channel_state = {
- # 基本测量数据
- "voltage": metrics.get("voltage_V", 0.0),
- "current": metrics.get("current_A", 0.0),
- "time": metrics.get("totaltime_s", 0.0),
-
- # 状态信息
- "status": status_channel.get("state", "unknown"),
- "color": status_channel.get("color", self.STATUS_COLOR["unknown"]),
-
- # 通道名称标识
- "Channel_Name": f"{self.machine_id}-{subdev_id}-{chl_id}",
-
- }
- r.load_state(channel_state)
-
- # 调试信息
- if self._ros_node and hasattr(self._ros_node, 'lab_logger'):
- self._ros_node.lab_logger().debug(
- f"更新P1资源状态: {resource_name} <- subdev{subdev_id}/chl{chl_id} "
- f"状态:{channel_state['status']}"
+ def _update_plate_resources(self, status_processed: Dict[int, Dict]):
+ """更新7台设备共14盘电池资源的状态"""
+ for devid in self.display_device_ids:
+ machine_data = status_processed.get(devid, {})
+ subunits = machine_data.get("subunits", {})
+ for plate_num, subdev_start, subdev_end in ((1, 1, 5), (2, 6, 10)):
+ for subdev_id in range(subdev_start, subdev_end + 1):
+ status_row = subunits.get(subdev_id, {})
+ for chl_id in range(1, 9):
+ try:
+ col_idx = chl_id - 1
+ row_idx = subdev_id - subdev_start
+ r, resource_name, possible_names = self._get_plate_resource(
+ devid=devid,
+ plate_num=plate_num,
+ row_idx=row_idx,
+ col_idx=col_idx
)
- else:
- # 如果找不到资源,记录调试信息
- if self._ros_node and hasattr(self._ros_node, 'lab_logger'):
- self._ros_node.lab_logger().debug(
- f"P1未找到资源: subdev{subdev_id}/chl{chl_id} -> 尝试的名称: {possible_names}"
- )
- except (KeyError, IndexError) as e:
- if self._ros_node and hasattr(self._ros_node, 'lab_logger'):
- self._ros_node.lab_logger().debug(f"P1映射错误: subdev{subdev_id}/chl{chl_id} - {e}")
- continue
-
- # 第2盘:subdevid 6-10 映射到 8列5行网格 (列0-7, 行0-4)
- for subdev_id in range(6, 11): # subdevid 6-10
- status_row = subunits.get(subdev_id, {})
-
- for chl_id in range(1, 9): # chlid 1-8
- try:
- col_idx = (chl_id - 1) # 0-7 (chlid 1-8 -> 列0-7)
- row_idx = (subdev_id - 6) # 0-4 (subdevid 6-10 -> 行0-4)
-
- # 尝试多种可能的资源命名格式
- possible_names = [
- f"P2_batterytestposition_{col_idx}_{row_idx}", # 用户提到的格式
- f"P2_{self.LETTERS[row_idx]}{col_idx + 1}", # 原有的A1-E8格式
- f"P2_{self.LETTERS[row_idx].lower()}{col_idx + 1}", # 小写字母格式
- ]
-
- r = None
- resource_name = None
- for name in possible_names:
- if name in self.station_resources:
- r = self.station_resources[name]
- resource_name = name
- break
-
- if r:
- status_channel = status_row.get(chl_id, {})
- metrics = status_channel.get("metrics", {})
- # 构建BatteryTestPosition状态数据(移除capacity和energy)
- channel_state = {
- # 基本测量数据
- "voltage": metrics.get("voltage_V", 0.0),
- "current": metrics.get("current_A", 0.0),
- "time": metrics.get("totaltime_s", 0.0),
-
- # 状态信息
- "status": status_channel.get("state", "unknown"),
- "color": status_channel.get("color", self.STATUS_COLOR["unknown"]),
-
- # 通道名称标识
- "Channel_Name": f"{self.machine_id}-{subdev_id}-{chl_id}",
-
- }
- r.load_state(channel_state)
-
- # 调试信息
- if self._ros_node and hasattr(self._ros_node, 'lab_logger'):
- self._ros_node.lab_logger().debug(
- f"更新P2资源状态: {resource_name} <- subdev{subdev_id}/chl{chl_id} "
- f"状态:{channel_state['status']}"
- )
- else:
- # 如果找不到资源,记录调试信息
- if self._ros_node and hasattr(self._ros_node, 'lab_logger'):
- self._ros_node.lab_logger().debug(
- f"P2未找到资源: subdev{subdev_id}/chl{chl_id} -> 尝试的名称: {possible_names}"
- )
- except (KeyError, IndexError) as e:
- if self._ros_node and hasattr(self._ros_node, 'lab_logger'):
- self._ros_node.lab_logger().debug(f"P2映射错误: subdev{subdev_id}/chl{chl_id} - {e}")
- continue
+ if r is None:
+ if self._ros_node and hasattr(self._ros_node, 'lab_logger'):
+ self._ros_node.lab_logger().debug(
+ f"{devid}_P{plate_num}未找到资源: subdev{subdev_id}/chl{chl_id} -> "
+ f"尝试的名称: {possible_names}"
+ )
+ continue
+ status_channel = status_row.get(chl_id, {})
+ metrics = status_channel.get("metrics", {})
+ channel_state = {
+ "voltage": metrics.get("voltage_V", 0.0),
+ "current": metrics.get("current_A", 0.0),
+ "time": metrics.get("totaltime_s", 0.0),
+ "status": status_channel.get("state", "unknown"),
+ "color": status_channel.get("color", self.STATUS_COLOR["unknown"]),
+ "Channel_Name": f"{devid}-{subdev_id}-{chl_id}",
+ }
+ r.load_state(channel_state)
+ if self._ros_node and hasattr(self._ros_node, 'lab_logger'):
+ self._ros_node.lab_logger().debug(
+ f"更新{devid}_P{plate_num}资源状态: {resource_name} <- "
+ f"subdev{subdev_id}/chl{chl_id} 状态:{channel_state['status']}"
+ )
+ except (KeyError, IndexError) as e:
+ if self._ros_node and hasattr(self._ros_node, 'lab_logger'):
+ self._ros_node.lab_logger().debug(
+ f"{devid}_P{plate_num}映射错误: subdev{subdev_id}/chl{chl_id} - {e}"
+ )
+ continue
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
"resources": list(self.station_resources.values())
})
@@ -640,6 +578,22 @@ class NewareBatteryTestSystem:
"""获取总通道数"""
return len(self._channels)
+ def _build_device_summary_dict(self) -> dict:
+ if not hasattr(self, '_channels') or not self._channels:
+ self._channels = self._build_channel_map()
+ channel_count_by_devid = {}
+ for channel in self._channels:
+ devid = channel.devid
+ channel_count_by_devid[devid] = channel_count_by_devid.get(devid, 0) + 1
+ return {
+ "channel_count_by_devid": channel_count_by_devid,
+ "display_device_ids": self.display_device_ids,
+ "total_channels": len(self._channels)
+ }
+
+ def device_summary(self) -> str:
+ return json.dumps(self._build_device_summary_dict(), ensure_ascii=False)
+
# ========================
# 设备动作方法(Uni-Lab标准)
# ========================
@@ -964,6 +918,7 @@ class NewareBatteryTestSystem:
'SIGR_LI': gen_mod.xml_SiGr_Li_Step,
'811_SIGR': gen_mod.xml_811_SiGr,
'811_CU_AGING': gen_mod.xml_811_Cu_aging,
+ 'ZQXNLRMO':gen_mod.xml_ZQXNLRMO,
}
if key not in fmap:
raise ValueError(f"未定义电池体系映射: {key}")
@@ -1141,16 +1096,7 @@ class NewareBatteryTestSystem:
dict: ROS2动作结果格式 {"return_info": str, "success": bool}
"""
try:
- # 确保_channels已初始化
- if not hasattr(self, '_channels') or not self._channels:
- self._channels = self._build_channel_map()
-
- summary = {}
- for channel in self._channels:
- devid = channel.devid
- summary[devid] = summary.get(devid, 0) + 1
-
- result_info = json.dumps(summary, ensure_ascii=False)
+ result_info = self.device_summary()
success_msg = f"设备摘要统计: {result_info}"
if self._ros_node:
self._ros_node.lab_logger().info(success_msg)
diff --git a/unilabos/devices/neware_battery_test_system/neware_driver.py b/unilabos/devices/neware_battery_test_system/neware_driver.py
new file mode 100644
index 00000000..5393892b
--- /dev/null
+++ b/unilabos/devices/neware_battery_test_system/neware_driver.py
@@ -0,0 +1,49 @@
+import socket
+END_MARKS = [b"\r\n#\r\n", b""] # 读到任一标志即可判定完整响应
+
+def build_start_command(devid, subdevid, chlid, CoinID,
+ ip_in_xml="127.0.0.1",
+ devtype:int=27,
+ recipe_path:str=f"D:\\HHM_test\\A001.xml",
+ backup_dir:str=f"D:\\HHM_test\\backup") -> str:
+ lines = [
+ '',
+ '',
+ ' start',
+ ' ',
+ f' {recipe_path}',
+ f' ',
+ '
',
+ '',
+ ]
+ # TCP 模式:请求必须以 #\r\n 结束(协议要求)
+ return "\r\n".join(lines) + "\r\n#\r\n"
+
+def recv_until_marks(sock: socket.socket, timeout=60):
+ sock.settimeout(timeout) # 上限给足,协议允许到 30s:contentReference[oaicite:2]{index=2}
+ buf = bytearray()
+ while True:
+ chunk = sock.recv(8192)
+ if not chunk:
+ break
+ buf += chunk
+ # 读到结束标志就停,避免等对端断开
+ for m in END_MARKS:
+ if m in buf:
+ return bytes(buf)
+ # 保险:读到完整 XML 结束标签也停
+ if b"" in buf:
+ return bytes(buf)
+ return bytes(buf)
+
+def start_test(ip="127.0.0.1", port=502, devid=3, subdevid=2, chlid=1, CoinID="A001", recipe_path=f"D:\\HHM_test\\A001.xml", backup_dir=f"D:\\HHM_test\\backup"):
+ xml_cmd = build_start_command(devid=devid, subdevid=subdevid, chlid=chlid, CoinID=CoinID, recipe_path=recipe_path, backup_dir=backup_dir)
+ #print(xml_cmd)
+ with socket.create_connection((ip, port), timeout=60) as s:
+ s.sendall(xml_cmd.encode("utf-8"))
+ data = recv_until_marks(s, timeout=60)
+ return data.decode("utf-8", errors="replace")
+
+if __name__ == "__main__":
+ resp = start_test(ip="127.0.0.1", port=502, devid=4, subdevid=10, chlid=1, CoinID="A001", recipe_path=f"D:\\HHM_test\\A001.xml", backup_dir=f"D:\\HHM_test\\backup")
+ print(resp)
diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py
index 0e577abc..a88c1b3f 100644
--- a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py
+++ b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py
@@ -1039,7 +1039,11 @@ class BioyondCellWorkstation(BioyondWorkstation):
logger.warning(f"[create_orders_formulation] 第 {idx + 1} 个配方无有效物料,跳过")
continue
- item_mix_time = mix_time[idx] if idx < len(mix_time) else 0
+ raw_mix_time = mix_time[idx] if idx < len(mix_time) else None
+ try:
+ item_mix_time = int(raw_mix_time) if raw_mix_time not in (None, "", "null") else 0
+ except (ValueError, TypeError):
+ item_mix_time = 0
logger.info(f"[create_orders_formulation] 第 {idx + 1} 个配方: orderName={order_name}, "
f"coinCellVolume={coin_cell_volume}, pouchCellVolume={pouch_cell_volume}, "
f"conductivityVolume={conductivity_volume}, totalMass={total_mass}, "
diff --git a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py
index b4333a57..9b511f4e 100644
--- a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py
+++ b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py
@@ -161,7 +161,9 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
logger.info("没有传入依华deck,检查启动json文件")
super().__init__(deck=deck, *args, **kwargs,)
self.debug_mode = debug_mode
-
+ self._modbus_address = address
+ self._modbus_port = port
+
""" 连接初始化 """
modbus_client = TCPClient(addr=address, port=port)
logger.debug(f"创建 Modbus 客户端: {modbus_client}")
@@ -178,9 +180,11 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
raise ValueError('modbus tcp connection failed')
self.nodes = BaseClient.load_csv(os.path.join(os.path.dirname(__file__), 'coin_cell_assembly_b.csv'))
self.client = modbus_client.register_node_list(self.nodes)
+ self._modbus_client_raw = modbus_client
else:
print("测试模式,跳过连接")
self.nodes, self.client = None, None
+ self._modbus_client_raw = None
""" 工站的配置 """
@@ -191,6 +195,32 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
self.csv_export_file = None
self.coin_num_N = 0 #已组装电池数量
+ def _ensure_modbus_connected(self) -> None:
+ """检查 Modbus TCP 连接是否存活,若已断开则自动重连(防止长时间空闲后连接超时)"""
+ if self.debug_mode or self._modbus_client_raw is None:
+ return
+ raw_client = self._modbus_client_raw.client
+ if raw_client.is_socket_open():
+ return
+ logger.warning("[Modbus] 检测到连接已断开,尝试重连...")
+ try:
+ raw_client.close()
+ except Exception:
+ pass
+ count = 10
+ while count > 0:
+ count -= 1
+ try:
+ raw_client.connect()
+ except Exception:
+ pass
+ if raw_client.is_socket_open():
+ break
+ time.sleep(2)
+ if not raw_client.is_socket_open():
+ raise RuntimeError(f"Modbus TCP 重连失败({self._modbus_address}:{self._modbus_port}),请检查设备连接")
+ logger.info("[Modbus] 重连成功")
+
def post_init(self, ros_node: ROS2WorkstationNode):
self._ros_node = ros_node
@@ -1056,6 +1086,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
# 步骤0: 前置条件检查
logger.info("\n【步骤 0/4】前置条件检查...")
+ self._ensure_modbus_connected()
try:
# 检查 REG_UNILAB_INTERACT (应该为False,表示使用Unilab交互)
unilab_interact_node = self.client.use_node('REG_UNILAB_INTERACT')
diff --git a/unilabos/registry/devices/coin_cell_workstation.yaml b/unilabos/registry/devices/coin_cell_workstation.yaml
index 5bdf56d5..d8158f17 100644
--- a/unilabos/registry/devices/coin_cell_workstation.yaml
+++ b/unilabos/registry/devices/coin_cell_workstation.yaml
@@ -102,7 +102,7 @@ coincellassemblyworkstation_device:
goal:
properties:
assembly_pressure:
- default: 4200
+ default: 3200
description: 电池压制力(N)
type: integer
assembly_type:
@@ -118,7 +118,7 @@ coincellassemblyworkstation_device:
description: 是否启用压力模式
type: boolean
dual_drop_first_volume:
- default: 25
+ default: 0
description: 二次滴液第一次排液体积(μL)
type: integer
dual_drop_mode:
@@ -137,6 +137,7 @@ coincellassemblyworkstation_device:
description: 电解液瓶数
type: string
elec_use_num:
+ default: 5
description: 每瓶电解液组装电池数
type: string
elec_vol:
@@ -144,7 +145,7 @@ coincellassemblyworkstation_device:
description: 电解液吸液量(μL)
type: integer
file_path:
- default: /Users/sml/work
+ default: D:\UniLabdev\Uni-Lab-OS\unilabos\devices\workstation\coin_cell_assembly
description: 实验记录保存路径
type: string
fujipian_juzhendianwei:
diff --git a/unilabos/registry/devices/neware_battery_test_system.yaml b/unilabos/registry/devices/neware_battery_test_system.yaml
index 4f3b972a..bd87e17d 100644
--- a/unilabos/registry/devices/neware_battery_test_system.yaml
+++ b/unilabos/registry/devices/neware_battery_test_system.yaml
@@ -324,7 +324,7 @@ neware_battery_test_system:
status_types:
channel_status: Dict[int, Dict]
connection_info: Dict[str, str]
- device_summary: dict
+ device_summary: str
status: str
total_channels: int
type: python
@@ -339,9 +339,18 @@ neware_battery_test_system:
type: string
ip:
type: string
- machine_id:
- default: 1
- type: integer
+ machine_ids:
+ default:
+ - 1
+ - 2
+ - 3
+ - 4
+ - 5
+ - 6
+ - 86
+ items:
+ type: integer
+ type: array
oss_prefix:
default: neware_backup
type: string
@@ -374,7 +383,7 @@ neware_battery_test_system:
type: string
type: object
device_summary:
- type: object
+ type: string
status:
type: string
total_channels: