feat(opcua): 增强节点ID解析兼容性和数据类型处理

改进节点ID解析逻辑以支持多种格式,包括字符串和数字标识符
添加数据类型转换处理,确保写入值时类型匹配
优化错误提示信息,便于调试节点连接问题
This commit is contained in:
ZiWei
2025-11-26 19:57:06 +08:00
parent dbe129caab
commit fb6ee79577
2 changed files with 603 additions and 312 deletions

View File

@@ -125,6 +125,9 @@ class BaseClient(UniversalDriver):
# 初始化名称映射字典 # 初始化名称映射字典
self._name_mapping = {} self._name_mapping = {}
self._reverse_mapping = {} self._reverse_mapping = {}
# 初始化线程锁(在子类中会被重新创建,这里提供默认实现)
import threading
self._client_lock = threading.RLock()
def _set_client(self, client: Optional[Client]) -> None: def _set_client(self, client: Optional[Client]) -> None:
if client is None: if client is None:
@@ -152,15 +155,24 @@ class BaseClient(UniversalDriver):
if not self.client: if not self.client:
raise ValueError('client is not connected') raise ValueError('client is not connected')
logger.info('开始查找节点...') logger.info(f'开始查找 {len(self._variables_to_find)}节点...')
try: try:
# 获取根节点 # 获取根节点
root = self.client.get_root_node() root = self.client.get_root_node()
objects = root.get_child(["0:Objects"]) objects = root.get_child(["0:Objects"])
# 记录查找前的状态
before_count = len(self._node_registry)
# 查找节点 # 查找节点
self._find_nodes_recursive(objects) self._find_nodes_recursive(objects)
# 记录查找后的状态
after_count = len(self._node_registry)
newly_found = after_count - before_count
logger.info(f"本次查找新增 {newly_found} 个节点,当前共 {after_count}")
# 检查是否所有节点都已找到 # 检查是否所有节点都已找到
not_found = [] not_found = []
for var_name, var_info in self._variables_to_find.items(): for var_name, var_info in self._variables_to_find.items():
@@ -168,9 +180,13 @@ class BaseClient(UniversalDriver):
not_found.append(var_name) not_found.append(var_name)
if not_found: if not_found:
logger.warning(f"以下节点未找到: {', '.join(not_found)}") logger.warning(f"⚠ 以下 {len(not_found)}节点未找到: {', '.join(not_found[:10])}{'...' if len(not_found) > 10 else ''}")
logger.warning(f"提示:请检查这些节点名称是否与服务器的 BrowseName 完全匹配(包括大小写、空格等)")
# 提供一个示例来帮助调试
if not_found:
logger.info(f"尝试在服务器中查找第一个未找到的节点 '{not_found[0]}' 的相似节点...")
else: else:
logger.info("所有节点均已找到") logger.info(f"✓ 所有 {len(self._variables_to_find)}节点均已找到并注册")
except Exception as e: except Exception as e:
logger.error(f"查找节点失败: {e}") logger.error(f"查找节点失败: {e}")
@@ -188,17 +204,18 @@ class BaseClient(UniversalDriver):
var_info = self._variables_to_find[node_name] var_info = self._variables_to_find[node_name]
node_type = var_info.get("node_type") node_type = var_info.get("node_type")
data_type = var_info.get("data_type") data_type = var_info.get("data_type")
node_id_str = str(node.nodeid)
# 根据节点类型创建相应的对象 # 根据节点类型创建相应的对象
if node_type == NodeType.VARIABLE: if node_type == NodeType.VARIABLE:
self._node_registry[node_name] = Variable(self.client, node_name, str(node.nodeid), data_type) self._node_registry[node_name] = Variable(self.client, node_name, node_id_str, data_type)
logger.info(f"找到变量节点: {node_name}") logger.info(f"找到变量节点: '{node_name}', NodeId: {node_id_str}, DataType: {data_type}")
elif node_type == NodeType.METHOD: elif node_type == NodeType.METHOD:
# 对于方法节点需要获取父节点ID # 对于方法节点需要获取父节点ID
parent_node = node.get_parent() parent_node = node.get_parent()
parent_node_id = str(parent_node.nodeid) parent_node_id = str(parent_node.nodeid)
self._node_registry[node_name] = Method(self.client, node_name, str(node.nodeid), parent_node_id, data_type) self._node_registry[node_name] = Method(self.client, node_name, node_id_str, parent_node_id, data_type)
logger.info(f"找到方法节点: {node_name}") logger.info(f"找到方法节点: '{node_name}', NodeId: {node_id_str}, ParentId: {parent_node_id}")
# 递归处理子节点 # 递归处理子节点
for child in node.get_children(): for child in node.get_children():
@@ -296,13 +313,17 @@ class BaseClient(UniversalDriver):
if name in self._name_mapping: if name in self._name_mapping:
chinese_name = self._name_mapping[name] chinese_name = self._name_mapping[name]
if chinese_name in self._node_registry: if chinese_name in self._node_registry:
return self._node_registry[chinese_name] node = self._node_registry[chinese_name]
logger.debug(f"使用节点: '{name}' -> '{chinese_name}', NodeId: {node.node_id}")
return node
elif chinese_name in self._variables_to_find: elif chinese_name in self._variables_to_find:
logger.warning(f"节点 {chinese_name} (英文名: {name}) 尚未找到,尝试重新查找") logger.warning(f"节点 {chinese_name} (英文名: {name}) 尚未找到,尝试重新查找")
if self.client: if self.client:
self._find_nodes() self._find_nodes()
if chinese_name in self._node_registry: if chinese_name in self._node_registry:
return self._node_registry[chinese_name] node = self._node_registry[chinese_name]
logger.info(f"重新查找成功: '{chinese_name}', NodeId: {node.node_id}")
return node
raise ValueError(f'节点 {chinese_name} (英文名: {name}) 未注册或未找到') raise ValueError(f'节点 {chinese_name} (英文名: {name}) 未注册或未找到')
# 直接使用原始名称查找 # 直接使用原始名称查找
@@ -312,9 +333,14 @@ class BaseClient(UniversalDriver):
if self.client: if self.client:
self._find_nodes() self._find_nodes()
if name in self._node_registry: if name in self._node_registry:
return self._node_registry[name] node = self._node_registry[name]
logger.info(f"重新查找成功: '{name}', NodeId: {node.node_id}")
return node
logger.error(f"❌ 节点 '{name}' 未注册或未找到。已注册节点: {list(self._node_registry.keys())[:5]}...")
raise ValueError(f'节点 {name} 未注册或未找到') raise ValueError(f'节点 {name} 未注册或未找到')
return self._node_registry[name] node = self._node_registry[name]
logger.debug(f"使用节点: '{name}', NodeId: {node.node_id}")
return node
def get_node_registry(self) -> Dict[str, OpcUaNodeBase]: def get_node_registry(self) -> Dict[str, OpcUaNodeBase]:
return self._node_registry return self._node_registry
@@ -335,12 +361,13 @@ class BaseClient(UniversalDriver):
return self return self
logger.info(f'开始注册 {len(node_list)} 个节点...') logger.info(f'开始注册 {len(node_list)} 个节点...')
new_nodes_count = 0
for node in node_list: for node in node_list:
if node is None: if node is None:
continue continue
if node.name in self._node_registry: if node.name in self._node_registry:
logger.info(f'节点 {node.name} 已存在') logger.debug(f'节点 "{node.name}" 已存在于注册表')
exist = self._node_registry[node.name] exist = self._node_registry[node.name]
if exist.type != node.node_type: if exist.type != node.node_type:
raise ValueError(f'节点 {node.name} 类型 {node.node_type} 与已存在的类型 {exist.type} 不一致') raise ValueError(f'节点 {node.name} 类型 {node.node_type} 与已存在的类型 {exist.type} 不一致')
@@ -351,9 +378,10 @@ class BaseClient(UniversalDriver):
"node_type": node.node_type, "node_type": node.node_type,
"data_type": node.data_type "data_type": node.data_type
} }
logger.info(f'添加节点 {node.name} 到待查找列表') new_nodes_count += 1
logger.debug(f'添加节点 "{node.name}" ({node.node_type}) 到待查找列表')
logger.info('节点注册完成') logger.info(f'节点注册完成:新增 {new_nodes_count} 个待查找节点,总计 {len(self._variables_to_find)}')
# 如果客户端已连接,立即开始查找 # 如果客户端已连接,立即开始查找
if self.client: if self.client:
@@ -518,15 +546,25 @@ class BaseClient(UniversalDriver):
raise ValueError("必须提供write_nodes参数") raise ValueError("必须提供write_nodes参数")
def execute_init_function(use_node: Callable[[str], OpcUaNodeBase]) -> bool: def execute_init_function(use_node: Callable[[str], OpcUaNodeBase]) -> bool:
if isinstance(write_nodes, list): """根据 _workflow_params 为各节点写入真实数值。
# 处理节点列表
for node_name in write_nodes:
# 尝试从参数中获取同名参数的值
current_value = True # 默认值
if hasattr(self, '_workflow_params') and node_name in self._workflow_params:
current_value = self._workflow_params[node_name]
print(f"初始化函数: 从参数获取值 {node_name} = {current_value}")
约定:
- write_nodes 为 list 时: 节点名 == 参数名,从 _workflow_params[node_name] 取值;
- write_nodes 为 dict 时:
* value 为字符串且在 _workflow_params 中: 当作参数名去取值;
* 否则 value 视为常量直接写入。
"""
params = getattr(self, "_workflow_params", {}) or {}
if isinstance(write_nodes, list):
# 节点列表形式: 节点名与参数名一致
for node_name in write_nodes:
if node_name not in params:
print(f"初始化函数: 参数中未找到 {node_name}, 跳过写入")
continue
current_value = params[node_name]
print(f"初始化函数: 写入节点 {node_name} = {current_value}") print(f"初始化函数: 写入节点 {node_name} = {current_value}")
input_json = json.dumps({"node_name": node_name, "value": current_value}) input_json = json.dumps({"node_name": node_name, "value": current_value})
result_str = self.write_node(input_json) result_str = self.write_node(input_json)
@@ -538,13 +576,14 @@ class BaseClient(UniversalDriver):
except Exception as e: except Exception as e:
print(f"初始化函数: 解析写入结果失败: {e}, 原始结果: {result_str}") print(f"初始化函数: 解析写入结果失败: {e}, 原始结果: {result_str}")
elif isinstance(write_nodes, dict): elif isinstance(write_nodes, dict):
# 处理节点字典,使用指定的值 # 映射形式: 节点名 -> 参数名或常量
for node_name, node_value in write_nodes.items(): for node_name, node_value in write_nodes.items():
# 检查值是否是字符串类型的参数名 if isinstance(node_value, str) and node_value in params:
current_value = node_value current_value = params[node_value]
if isinstance(node_value, str) and hasattr(self, '_workflow_params') and node_value in self._workflow_params:
current_value = self._workflow_params[node_value]
print(f"初始化函数: 从参数获取值 {node_value} = {current_value}") print(f"初始化函数: 从参数获取值 {node_value} = {current_value}")
else:
current_value = node_value
print(f"初始化函数: 使用常量值 写入 {node_name} = {current_value}")
print(f"初始化函数: 写入节点 {node_name} = {current_value}") print(f"初始化函数: 写入节点 {node_name} = {current_value}")
input_json = json.dumps({"node_name": node_name, "value": current_value}) input_json = json.dumps({"node_name": node_name, "value": current_value})
@@ -672,20 +711,20 @@ class BaseClient(UniversalDriver):
condition_nodes: 条件节点列表 [节点名1, 节点名2] condition_nodes: 条件节点列表 [节点名1, 节点名2]
""" """
def execute_start_function(use_node: Callable[[str], OpcUaNodeBase]) -> bool: def execute_start_function(use_node: Callable[[str], OpcUaNodeBase]) -> bool:
# 直接处理写入节点 """开始函数: 写入触发节点, 然后轮询条件节点直到满足停止条件。"""
params = getattr(self, "_workflow_params", {}) or {}
# 先处理写入节点(触发位等)
if write_nodes: if write_nodes:
if isinstance(write_nodes, list): if isinstance(write_nodes, list):
# 处理节点列表,默认值都是True # 列表形式: 节点名与参数名一致, 若无参数则直接写 True
for i, node_name in enumerate(write_nodes): for node_name in write_nodes:
# 尝试获取与节点对应的参数值 if node_name in params:
param_name = f"write_{i}" current_value = params[node_name]
else:
current_value = True
# 获取参数值(如果有)
current_value = True # 默认值
if hasattr(self, '_workflow_params') and param_name in self._workflow_params:
current_value = self._workflow_params[param_name]
# 直接写入节点
print(f"直接写入节点 {node_name} = {current_value}") print(f"直接写入节点 {node_name} = {current_value}")
input_json = json.dumps({"node_name": node_name, "value": current_value}) input_json = json.dumps({"node_name": node_name, "value": current_value})
result_str = self.write_node(input_json) result_str = self.write_node(input_json)
@@ -697,14 +736,13 @@ class BaseClient(UniversalDriver):
except Exception as e: except Exception as e:
print(f"解析直接写入结果失败: {e}, 原始结果: {result_str}") print(f"解析直接写入结果失败: {e}, 原始结果: {result_str}")
elif isinstance(write_nodes, dict): elif isinstance(write_nodes, dict):
# 处理节点字典,值是指定的 # 字典形式: 节点名 -> 常量值(如 True/False)
for node_name, node_value in write_nodes.items(): for node_name, node_value in write_nodes.items():
# 尝试获取参数值(如果节点名与参数名匹配) if node_name in params:
current_value = node_value # 使用指定的默认值 current_value = params[node_name]
if hasattr(self, '_workflow_params') and node_name in self._workflow_params: else:
current_value = self._workflow_params[node_name] current_value = node_value
# 直接写入节点
print(f"直接写入节点 {node_name} = {current_value}") print(f"直接写入节点 {node_name} = {current_value}")
input_json = json.dumps({"node_name": node_name, "value": current_value}) input_json = json.dumps({"node_name": node_name, "value": current_value})
result_str = self.write_node(input_json) result_str = self.write_node(input_json)
@@ -732,6 +770,7 @@ class BaseClient(UniversalDriver):
# 直接读取节点 # 直接读取节点
result_str = self.read_node(node_name) result_str = self.read_node(node_name)
try: try:
time.sleep(1)
result_str = result_str.replace("'", '"') result_str = result_str.replace("'", '"')
result_dict = json.loads(result_str) result_dict = json.loads(result_str)
read_res = result_dict.get("value") read_res = result_dict.get("value")
@@ -1035,31 +1074,33 @@ class BaseClient(UniversalDriver):
读取节点值的便捷方法 读取节点值的便捷方法
返回包含result字段的字典 返回包含result字段的字典
""" """
try: # 使用锁保护客户端访问
node = self.use_node(node_name) with self._client_lock:
value, error = node.read() try:
node = self.use_node(node_name)
value, error = node.read()
# 创建结果字典 # 创建结果字典
result = { result = {
"value": value, "value": value,
"error": error, "error": error,
"node_name": node_name, "node_name": node_name,
"timestamp": time.time() "timestamp": time.time()
} }
# 返回JSON字符串 # 返回JSON字符串
return json.dumps(result) return json.dumps(result)
except Exception as e: except Exception as e:
logger.error(f"读取节点 {node_name} 失败: {e}") logger.error(f"读取节点 {node_name} 失败: {e}")
# 创建错误结果字典 # 创建错误结果字典
result = { result = {
"value": None, "value": None,
"error": True, "error": True,
"node_name": node_name, "node_name": node_name,
"error_message": str(e), "error_message": str(e),
"timestamp": time.time() "timestamp": time.time()
} }
return json.dumps(result) return json.dumps(result)
def write_node(self, json_input: str) -> str: def write_node(self, json_input: str) -> str:
""" """
@@ -1068,47 +1109,49 @@ class BaseClient(UniversalDriver):
eg:'{\"node_name\":\"反应罐号码\",\"value\":\"2\"}' eg:'{\"node_name\":\"反应罐号码\",\"value\":\"2\"}'
返回JSON格式的字符串包含操作结果 返回JSON格式的字符串包含操作结果
""" """
try: # 使用锁保护客户端访问
# 解析JSON格式的输入 with self._client_lock:
if not isinstance(json_input, str):
json_input = str(json_input)
try: try:
input_data = json.loads(json_input) # 解析JSON格式的输入
if not isinstance(input_data, dict): if not isinstance(json_input, str):
return json.dumps({"error": True, "error_message": "输入必须是包含node_name和value的JSON对象", "success": False}) json_input = str(json_input)
# 从JSON中提取节点名称和值 try:
node_name = input_data.get("node_name") input_data = json.loads(json_input)
value = input_data.get("value") if not isinstance(input_data, dict):
return json.dumps({"error": True, "error_message": "输入必须是包含node_name和value的JSON对象", "success": False})
if node_name is None: # 从JSON中提取节点名称和值
return json.dumps({"error": True, "error_message": "JSON中缺少node_name字段", "success": False}) node_name = input_data.get("node_name")
except json.JSONDecodeError as e: value = input_data.get("value")
return json.dumps({"error": True, "error_message": f"JSON解析错误: {str(e)}", "success": False})
node = self.use_node(node_name) if node_name is None:
error = node.write(value) return json.dumps({"error": True, "error_message": "JSON中缺少node_name字段", "success": False})
except json.JSONDecodeError as e:
return json.dumps({"error": True, "error_message": f"JSON解析错误: {str(e)}", "success": False})
# 创建结果字典 node = self.use_node(node_name)
result = { error = node.write(value)
"value": value,
"error": error,
"node_name": node_name,
"timestamp": time.time(),
"success": not error
}
return json.dumps(result) # 创建结果字典
except Exception as e: result = {
logger.error(f"写入节点失败: {e}") "value": value,
result = { "error": error,
"error": True, "node_name": node_name,
"error_message": str(e), "timestamp": time.time(),
"timestamp": time.time(), "success": not error
"success": False }
}
return json.dumps(result) return json.dumps(result)
except Exception as e:
logger.error(f"写入节点失败: {e}")
result = {
"error": True,
"error_message": str(e),
"timestamp": time.time(),
"success": False
}
return json.dumps(result)
def call_method(self, node_name: str, *args) -> Tuple[Any, bool]: def call_method(self, node_name: str, *args) -> Tuple[Any, bool]:
""" """
@@ -1150,16 +1193,27 @@ class OpcUaClient(BaseClient):
self._refresh_running = False self._refresh_running = False
self._refresh_thread = None self._refresh_thread = None
# 添加线程锁保护OPC UA客户端的并发访问
import threading
self._client_lock = threading.RLock()
# 如果提供了配置文件路径,则加载配置并注册工作流 # 如果提供了配置文件路径,则加载配置并注册工作流
if config_path: if config_path:
self.load_config(config_path) self.load_config(config_path)
# 启动节点值刷新线程 # 延迟启动节点值刷新线程,确保节点查找完成
self.start_node_refresh() # 注意刷新线程会在所有节点注册后由load_config调用
# 暂时不在这里启动,避免在节点未找到时就开始刷新
# self.start_node_refresh()
def _register_nodes_as_attributes(self): def _register_nodes_as_attributes(self):
"""将所有节点注册为实例属性可以通过self.node_name访问""" """将所有节点注册为实例属性可以通过self.node_name访问"""
for node_name, node in self._node_registry.items(): for node_name, node in self._node_registry.items():
# 检查node_id是否有效
if not node.node_id or node.node_id == "":
logger.warning(f"⚠ 节点 '{node_name}' 的 node_id 为空,跳过注册为属性")
continue
# 检查是否有对应的英文名称 # 检查是否有对应的英文名称
eng_name = self._reverse_mapping.get(node_name) eng_name = self._reverse_mapping.get(node_name)
if eng_name: if eng_name:
@@ -1182,7 +1236,7 @@ class OpcUaClient(BaseClient):
# 使用property装饰器将方法注册为类属性 # 使用property装饰器将方法注册为类属性
setattr(OpcUaClient, attr_name, property(create_property_getter(node_name))) setattr(OpcUaClient, attr_name, property(create_property_getter(node_name)))
logger.info(f"已注册节点 '{node_name}' 为属性 '{attr_name}'") logger.debug(f"已注册节点 '{node_name}' 为属性 '{attr_name}', NodeId: {node.node_id}")
def refresh_node_values(self): def refresh_node_values(self):
"""刷新所有节点的值到缓存""" """刷新所有节点的值到缓存"""
@@ -1190,22 +1244,29 @@ class OpcUaClient(BaseClient):
logger.warning("客户端未初始化,无法刷新节点值") logger.warning("客户端未初始化,无法刷新节点值")
return return
try: # 使用锁保护客户端访问
# 简单检查连接状态,如果不连接会抛出异常 with self._client_lock:
self.client.get_namespace_array()
except Exception as e:
logger.warning(f"客户端连接异常,无法刷新节点值: {e}")
return
for node_name, node in self._node_registry.items():
try: try:
if hasattr(node, 'read'): # 简单检查连接状态,如果不连接会抛出异常
value, error = node.read() self.client.get_namespace_array()
if not error:
self._node_values[node_name] = value
#logger.debug(f"已刷新节点 '{node_name}' 的值: {value}")
except Exception as e: except Exception as e:
logger.error(f"刷新节点 '{node_name}' 失败: {e}") logger.warning(f"客户端连接异常,无法刷新节点值: {e}")
return
for node_name, node in self._node_registry.items():
try:
# 跳过node_id为空的节点
if not node.node_id or node.node_id == "":
continue
if hasattr(node, 'read'):
value, error = node.read()
if not error:
self._node_values[node_name] = value
#logger.debug(f"已刷新节点 '{node_name}' 的值: {value}")
except Exception as e:
# 降低日志级别,避免刷新线程产生大量错误日志
logger.debug(f"刷新节点 '{node_name}' 失败: {e}")
def get_node_value(self, name): def get_node_value(self, name):
"""获取节点值,支持中文名和英文名""" """获取节点值,支持中文名和英文名"""
@@ -1303,6 +1364,11 @@ class OpcUaClient(BaseClient):
# 直接使用字典 # 直接使用字典
self.register_node_list_from_csv_path(**config_data["register_node_list_from_csv_path"]) self.register_node_list_from_csv_path(**config_data["register_node_list_from_csv_path"])
# 立即执行节点查找(如果客户端已连接)
if self.client and self._variables_to_find:
logger.info("CSV加载完成开始查找服务器节点...")
self._find_nodes()
# 处理工作流创建 # 处理工作流创建
if "create_flow" in config_data: if "create_flow" in config_data:
# 直接传递字典列表 # 直接传递字典列表
@@ -1310,14 +1376,114 @@ class OpcUaClient(BaseClient):
# 将工作流注册为实例方法 # 将工作流注册为实例方法
self.register_workflows_as_methods() self.register_workflows_as_methods()
# 将所有节点注册为属性 # 将所有节点注册为属性(只注册已找到的节点)
self._register_nodes_as_attributes() self._register_nodes_as_attributes()
# 打印节点注册统计
found_count = len(self._node_registry)
total_count = len(self._variables_to_find)
if found_count < total_count:
logger.warning(f"节点查找完成:找到 {found_count}/{total_count} 个节点")
logger.warning(f"{total_count - found_count} 个节点未找到,这些节点的操作将会失败")
else:
logger.info(f"✓ 节点查找完成:所有 {found_count} 个节点均已找到")
# 现在启动节点值刷新线程
# self.start_node_refresh()
logger.info(f"成功从 {config_path} 加载配置") logger.info(f"成功从 {config_path} 加载配置")
except Exception as e: except Exception as e:
logger.error(f"加载配置文件 {config_path} 失败: {e}") logger.error(f"加载配置文件 {config_path} 失败: {e}")
traceback.print_exc() traceback.print_exc()
def print_node_registry_status(self):
"""打印节点注册状态,用于调试"""
print("\n" + "="*80)
print("节点注册状态诊断报告")
print("="*80)
print(f"\n待查找节点总数: {len(self._variables_to_find)}")
print(f"已找到节点总数: {len(self._node_registry)}")
print(f"未找到节点总数: {len(self._variables_to_find) - len(self._node_registry)}")
# 显示已找到的节点前10个
if self._node_registry:
print(f"\n✓ 已找到的节点 (显示前10个):")
for i, (name, node) in enumerate(list(self._node_registry.items())[:10]):
eng_name = self._reverse_mapping.get(name, "")
eng_info = f" ({eng_name})" if eng_name else ""
print(f" {i+1}. '{name}'{eng_info}")
print(f" NodeId: {node.node_id}")
print(f" Type: {node.type}")
# 显示未找到的节点
not_found = [name for name in self._variables_to_find if name not in self._node_registry]
if not_found:
print(f"\n✗ 未找到的节点 (显示前20个):")
for i, name in enumerate(not_found[:20]):
eng_name = self._reverse_mapping.get(name, "")
eng_info = f" ({eng_name})" if eng_name else ""
node_info = self._variables_to_find[name]
print(f" {i+1}. '{name}'{eng_info} - {node_info['node_type']}")
print("\n" + "="*80)
print("提示:")
print("1. 如果大量节点未找到请检查CSV中的节点名称是否与服务器BrowseName完全匹配")
print("2. 可以使用 client.browse_server_nodes() 查看服务器的实际节点结构")
print("3. 节点名称区分大小写,且包括所有空格和特殊字符")
print("="*80 + "\n")
def browse_server_nodes(self, max_depth=3, start_path=["0:Objects"]):
"""浏览服务器节点树,用于调试和对比"""
if not self.client:
print("客户端未连接")
return
print("\n" + "="*80)
print(f"服务器节点浏览 (最大深度: {max_depth})")
print("="*80 + "\n")
try:
root = self.client.get_root_node()
start_node = root.get_child(start_path)
self._browse_node_recursive(start_node, depth=0, max_depth=max_depth)
except Exception as e:
print(f"浏览失败: {e}")
traceback.print_exc()
def _browse_node_recursive(self, node, depth=0, max_depth=3):
"""递归浏览节点"""
if depth > max_depth:
return
try:
browse_name = node.get_browse_name()
node_class = node.get_node_class()
indent = " " * depth
# 显示节点信息
print(f"{indent}├─ {browse_name.Name}")
print(f"{indent}│ NodeId: {str(node.nodeid)}")
print(f"{indent}│ NodeClass: {node_class}")
# 如果是变量,显示数据类型
if node_class == NodeClass.Variable:
try:
data_type = node.get_data_type()
print(f"{indent}│ DataType: {data_type}")
except:
pass
# 递归处理子节点(限制数量避免输出过多)
if depth < max_depth:
children = node.get_children()
for i, child in enumerate(children[:20]): # 每层最多显示20个子节点
self._browse_node_recursive(child, depth + 1, max_depth)
if len(children) > 20:
print(f"{indent} ... ({len(children) - 20} more children)")
except Exception as e:
# 忽略单个节点的错误
pass
def disconnect(self): def disconnect(self):
# 停止刷新线程 # 停止刷新线程
self.stop_node_refresh() self.stop_node_refresh()
@@ -1338,8 +1504,8 @@ if __name__ == '__main__':
# 创建OPC UA客户端并加载配置 # 创建OPC UA客户端并加载配置
try: try:
client = OpcUaClient( client = OpcUaClient(
url="opc.tcp://localhost:4840/freeopcua/server/", # 替换为实际的OPC UA服务器地址 url="opc.tcp://192.168.1.88:4840/freeopcua/server/", # 替换为实际的OPC UA服务器地址
config_path=config_path # 传入配置文件路径 config_path="D:\\Uni-Lab-OS\\unilabos\\device_comms\\opcua_client\\opcua_huairou.json" # 传入配置文件路径
) )
# 列出所有已注册的工作流 # 列出所有已注册的工作流
@@ -1349,13 +1515,14 @@ if __name__ == '__main__':
# 测试trigger_grab_action工作流 - 使用英文参数名 # 测试trigger_grab_action工作流 - 使用英文参数名
print("\n测试trigger_grab_action工作流 - 使用英文参数名:") print("\n测试trigger_grab_action工作流 - 使用英文参数名:")
client.trigger_grab_action(reaction_tank_number=2, raw_tank_number=3) client.trigger_grab_action(reaction_tank_number=2, raw_tank_number=2)
# client.set_node_value("reaction_tank_number", 2)
# 读取节点值 - 使用英文节点名 # 读取节点值 - 使用英文节点名
grab_complete = client.get_node_value("grab_complete") grab_complete = client.get_node_value("grab_complete")
reaction_tank = client.get_node_value("reaction_tank_number") reaction_tank = client.get_node_value("reaction_tank_number")
raw_tank = client.get_node_value("raw_tank_number") raw_tank = client.get_node_value("raw_tank_number")
print(f"\n执行后状态检查 (使用英文节点名):") print(f"\n执行后状态检查 (使用英文节点名):")
print(f" - 抓取完成状态: {grab_complete}") print(f" - 抓取完成状态: {grab_complete}")
print(f" - 当前反应罐号码: {reaction_tank}") print(f" - 当前反应罐号码: {reaction_tank}")

View File

@@ -3,7 +3,7 @@ from enum import Enum
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Tuple, Union, Optional, Any, List from typing import Tuple, Union, Optional, Any, List
from opcua import Client, Node from opcua import Client, Node, ua
from opcua.ua import NodeId, NodeClass, VariantType from opcua.ua import NodeId, NodeClass, VariantType
@@ -47,23 +47,68 @@ class Base(ABC):
def _get_node(self) -> Node: def _get_node(self) -> Node:
if self._node is None: if self._node is None:
try: try:
# 检查是否是NumericNodeId(ns=X;i=Y)格式 # 尝试多种 NodeId 字符串格式解析,兼容不同服务器/库的输出
if "NumericNodeId" in self._node_id: # 可能的格式示例: 'ns=2;i=1234', 'ns=2;s=SomeString',
# 从字符串中提取命名空间和标识符 # 'StringNodeId(ns=4;s=OPC|变量名)', 'NumericNodeId(ns=2;i=1234)' 等
import re import re
match = re.search(r'ns=(\d+);i=(\d+)', self._node_id)
if match: nid = self._node_id
ns = int(match.group(1)) # 如果已经是 NodeId/Node 对象(库用户可能传入),直接使用
identifier = int(match.group(2)) try:
node_id = NodeId(identifier, ns) from opcua.ua import NodeId as UaNodeId
self._node = self._client.get_node(node_id) if isinstance(nid, UaNodeId):
self._node = self._client.get_node(nid)
return self._node
except Exception:
# 若导入或类型判断失败,则继续下一步
pass
# 直接以字符串形式处理
if isinstance(nid, str):
nid = nid.strip()
# 处理包含类名的格式,如 'StringNodeId(ns=4;s=...)' 或 'NumericNodeId(ns=2;i=...)'
# 提取括号内的内容
match_wrapped = re.match(r'(String|Numeric|Byte|Guid|TwoByteNode|FourByteNode)NodeId\((.*)\)', nid)
if match_wrapped:
# 提取括号内的实际 node_id 字符串
nid = match_wrapped.group(2).strip()
# 常见短格式 'ns=2;i=1234' 或 'ns=2;s=SomeString'
if re.match(r'^ns=\d+;[is]=', nid):
self._node = self._client.get_node(nid)
else: else:
raise ValueError(f"无法解析节点ID: {self._node_id}") # 尝试提取 ns 和 i 或 s
# 对于字符串标识符,可能包含特殊字符,使用非贪婪匹配
m_num = re.search(r'ns=(\d+);i=(\d+)', nid)
m_str = re.search(r'ns=(\d+);s=(.+?)(?:\)|$)', nid)
if m_num:
ns = int(m_num.group(1))
identifier = int(m_num.group(2))
node_id = NodeId(identifier, ns)
self._node = self._client.get_node(node_id)
elif m_str:
ns = int(m_str.group(1))
identifier = m_str.group(2).strip()
# 对于字符串标识符,直接使用字符串格式
node_id_str = f"ns={ns};s={identifier}"
self._node = self._client.get_node(node_id_str)
else:
# 回退:尝试直接传入字符串(有些实现接受其它格式)
try:
self._node = self._client.get_node(self._node_id)
except Exception as e:
# 输出更详细的错误信息供调试
print(f"获取节点失败(尝试直接字符串): {self._node_id}, 错误: {e}")
raise
else: else:
# 直接使用节点ID字符串 # 非字符串,尝试直接使用
self._node = self._client.get_node(self._node_id) self._node = self._client.get_node(self._node_id)
except Exception as e: except Exception as e:
print(f"获取节点失败: {self._node_id}, 错误: {e}") print(f"获取节点失败: {self._node_id}, 错误: {e}")
# 添加额外提示,帮助定位 BadNodeIdUnknown 问题
print("提示: 请确认该 node_id 是否来自当前连接的服务器地址空间," \
"以及 CSV/配置中名称与服务器 BrowseName 是否匹配。")
raise raise
return self._node return self._node
@@ -104,7 +149,56 @@ class Variable(Base):
def write(self, value: Any) -> bool: def write(self, value: Any) -> bool:
try: try:
self._get_node().set_value(value) # 如果声明了数据类型,则尝试转换并使用对应的 Variant 写入
coerced = value
try:
if self._data_type is not None:
# 基于声明的数据类型做简单类型转换
dt = self._data_type
if dt in (DataType.SBYTE, DataType.BYTE, DataType.INT16, DataType.UINT16,
DataType.INT32, DataType.UINT32, DataType.INT64, DataType.UINT64):
# 数值类型 -> int
if isinstance(value, str):
coerced = int(value)
else:
coerced = int(value)
elif dt in (DataType.FLOAT, DataType.DOUBLE):
if isinstance(value, str):
coerced = float(value)
else:
coerced = float(value)
elif dt == DataType.BOOLEAN:
if isinstance(value, str):
v = value.strip().lower()
if v in ("true", "1", "yes", "on"):
coerced = True
elif v in ("false", "0", "no", "off"):
coerced = False
else:
coerced = bool(value)
else:
coerced = bool(value)
elif dt == DataType.STRING or dt == DataType.BYTESTRING or dt == DataType.DATETIME:
coerced = str(value)
# 使用 ua.Variant 明确指定 VariantType
try:
variant = ua.Variant(coerced, dt.value)
self._get_node().set_value(variant)
except Exception:
# 回退:有些 set_value 实现接受 (value, variant_type)
try:
self._get_node().set_value(coerced, dt.value)
except Exception:
# 最后回退到直接写入(保持兼容性)
self._get_node().set_value(coerced)
else:
# 未声明数据类型,直接写入
self._get_node().set_value(value)
except Exception:
# 若在转换或按数据类型写入失败,尝试直接写入原始值并让上层捕获错误
self._get_node().set_value(value)
return False return False
except Exception as e: except Exception as e:
print(f"写入变量 {self._name} 失败: {e}") print(f"写入变量 {self._name} 失败: {e}")
@@ -120,20 +214,50 @@ class Method(Base):
def _get_parent_node(self) -> Node: def _get_parent_node(self) -> Node:
if self._parent_node is None: if self._parent_node is None:
try: try:
# 检查是否是NumericNodeId(ns=X;i=Y)格式 # 处理父节点ID使用与_get_node相同的解析逻辑
if "NumericNodeId" in self._parent_node_id: import re
# 从字符串中提取命名空间和标识符
import re nid = self._parent_node_id
match = re.search(r'ns=(\d+);i=(\d+)', self._parent_node_id)
if match: # 如果已经是 NodeId 对象,直接使用
ns = int(match.group(1)) try:
identifier = int(match.group(2)) from opcua.ua import NodeId as UaNodeId
node_id = NodeId(identifier, ns) if isinstance(nid, UaNodeId):
self._parent_node = self._client.get_node(node_id) self._parent_node = self._client.get_node(nid)
return self._parent_node
except Exception:
pass
# 字符串处理
if isinstance(nid, str):
nid = nid.strip()
# 处理包含类名的格式
match_wrapped = re.match(r'(String|Numeric|Byte|Guid|TwoByteNode|FourByteNode)NodeId\((.*)\)', nid)
if match_wrapped:
nid = match_wrapped.group(2).strip()
# 常见短格式
if re.match(r'^ns=\d+;[is]=', nid):
self._parent_node = self._client.get_node(nid)
else: else:
raise ValueError(f"无法解析父节点ID: {self._parent_node_id}") # 提取 ns 和 i 或 s
m_num = re.search(r'ns=(\d+);i=(\d+)', nid)
m_str = re.search(r'ns=(\d+);s=(.+?)(?:\)|$)', nid)
if m_num:
ns = int(m_num.group(1))
identifier = int(m_num.group(2))
node_id = NodeId(identifier, ns)
self._parent_node = self._client.get_node(node_id)
elif m_str:
ns = int(m_str.group(1))
identifier = m_str.group(2).strip()
node_id_str = f"ns={ns};s={identifier}"
self._parent_node = self._client.get_node(node_id_str)
else:
# 回退
self._parent_node = self._client.get_node(self._parent_node_id)
else: else:
# 直接使用节点ID字符串
self._parent_node = self._client.get_node(self._parent_node_id) self._parent_node = self._client.get_node(self._parent_node_id)
except Exception as e: except Exception as e:
print(f"获取父节点失败: {self._parent_node_id}, 错误: {e}") print(f"获取父节点失败: {self._parent_node_id}, 错误: {e}")