mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-04-24 01:03:57 +00:00
feat: 更新Neware电池测试系统驱动及电芯组装工作站相关文件
- 更新 neware_battery_test_system 驱动及设备配置 - 新增 generate_xml_content.py 工具脚本 - 更新 bioyond_cell_workstation 工作站实现 - 更新 coin_cell_assembly 扣式电池组装逻辑 - 更新相关注册表 YAML 配置:neware_battery_test_system、coin_cell_workstation、bioyond_cell
This commit is contained in:
@@ -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": []
|
||||
}
|
||||
}
|
||||
|
||||
1361
unilabos/devices/neware_battery_test_system/generate_xml_content.py
Normal file
1361
unilabos/devices/neware_battery_test_system/generate_xml_content.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
|
||||
49
unilabos/devices/neware_battery_test_system/neware_driver.py
Normal file
49
unilabos/devices/neware_battery_test_system/neware_driver.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import socket
|
||||
END_MARKS = [b"\r\n#\r\n", b"</bts>"] # 读到任一标志即可判定完整响应
|
||||
|
||||
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 = [
|
||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||
'<bts version="1.0">',
|
||||
' <cmd>start</cmd>',
|
||||
' <list count="1">',
|
||||
f' <start ip="{ip_in_xml}" devtype="{devtype}" devid="{devid}" subdevid="{subdevid}" chlid="{chlid}" barcode="{CoinID}">{recipe_path}</start>',
|
||||
f' <backup backupdir="{backup_dir}" remotedir="" filenametype="1" customfilename="" createdirbydate="0" filetype="0" backupontime="1" backupontimeinterval="1" backupfree="0" />',
|
||||
' </list>',
|
||||
'</bts>',
|
||||
]
|
||||
# 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"</bts>" 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)
|
||||
@@ -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}, "
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user